Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									6c68583a42
								
							
						
					
					
						commit
						2994a84f01
					
				|  | @ -4564,7 +4564,6 @@ Layout/LineLength: | ||||||
|     - 'spec/services/packages/nuget/search_service_spec.rb' |     - 'spec/services/packages/nuget/search_service_spec.rb' | ||||||
|     - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' |     - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' | ||||||
|     - 'spec/services/packages/rubygems/process_gem_service_spec.rb' |     - 'spec/services/packages/rubygems/process_gem_service_spec.rb' | ||||||
|     - 'spec/services/packages/terraform_module/create_package_service_spec.rb' |  | ||||||
|     - 'spec/services/personal_access_tokens/create_service_spec.rb' |     - 'spec/services/personal_access_tokens/create_service_spec.rb' | ||||||
|     - 'spec/services/post_receive_service_spec.rb' |     - 'spec/services/post_receive_service_spec.rb' | ||||||
|     - 'spec/services/projects/apple_target_platform_detector_service_spec.rb' |     - 'spec/services/projects/apple_target_platform_detector_service_spec.rb' | ||||||
|  |  | ||||||
|  | @ -3690,15 +3690,11 @@ RSpec/FeatureCategory: | ||||||
|     - 'spec/lib/gitlab/lazy_spec.rb' |     - 'spec/lib/gitlab/lazy_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/client_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/client_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb' |  | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb' |  | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/project_creator_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/project_creator_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb' |  | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb' |  | ||||||
|     - 'spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb' |     - 'spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb' | ||||||
|     - 'spec/lib/gitlab/lets_encrypt/challenge_spec.rb' |     - 'spec/lib/gitlab/lets_encrypt/challenge_spec.rb' | ||||||
|     - 'spec/lib/gitlab/lets_encrypt/client_spec.rb' |     - 'spec/lib/gitlab/lets_encrypt/client_spec.rb' | ||||||
|  |  | ||||||
|  | @ -46,6 +46,7 @@ export default () => { | ||||||
|         gl.mrWidgetData.can_create_pipeline_in_target_project, |         gl.mrWidgetData.can_create_pipeline_in_target_project, | ||||||
|       ), |       ), | ||||||
|       commitPathTemplate: gl.mrWidgetData.commit_path_template, |       commitPathTemplate: gl.mrWidgetData.commit_path_template, | ||||||
|  |       canAdminVulnerability: gl.mrWidgetData.can_admin_vulnerability, | ||||||
|       dismissalDescriptions, |       dismissalDescriptions, | ||||||
|     }, |     }, | ||||||
|     ...MrWidgetOptions, |     ...MrWidgetOptions, | ||||||
|  |  | ||||||
|  | @ -43,6 +43,7 @@ export default { | ||||||
|       :key="opt.value" |       :key="opt.value" | ||||||
|       :disabled="!!opt.disabled" |       :disabled="!!opt.disabled" | ||||||
|       :selected="value === opt.value" |       :selected="value === opt.value" | ||||||
|  |       v-bind="opt.props" | ||||||
|       @click="$emit('input', opt.value)" |       @click="$emit('input', opt.value)" | ||||||
|     > |     > | ||||||
|       <slot name="button-content" v-bind="opt">{{ opt.text }}</slot> |       <slot name="button-content" v-bind="opt">{{ opt.text }}</slot> | ||||||
|  |  | ||||||
|  | @ -51,6 +51,16 @@ module Mutations | ||||||
|                 required: false, |                 required: false, | ||||||
|                 description: copy_field_description(Types::Namespace::PackageSettingsType, :nuget_duplicate_exception_regex) |                 description: copy_field_description(Types::Namespace::PackageSettingsType, :nuget_duplicate_exception_regex) | ||||||
| 
 | 
 | ||||||
|  |         argument :terraform_module_duplicates_allowed, | ||||||
|  |                 GraphQL::Types::Boolean, | ||||||
|  |                 required: false, | ||||||
|  |                 description: copy_field_description(Types::Namespace::PackageSettingsType, :terraform_module_duplicates_allowed) | ||||||
|  | 
 | ||||||
|  |         argument :terraform_module_duplicate_exception_regex, | ||||||
|  |                 Types::UntrustedRegexp, | ||||||
|  |                 required: false, | ||||||
|  |                 description: copy_field_description(Types::Namespace::PackageSettingsType, :terraform_module_duplicate_exception_regex) | ||||||
|  | 
 | ||||||
|         argument :maven_package_requests_forwarding, |         argument :maven_package_requests_forwarding, | ||||||
|                 GraphQL::Types::Boolean, |                 GraphQL::Types::Boolean, | ||||||
|                 required: false, |                 required: false, | ||||||
|  |  | ||||||
|  | @ -35,6 +35,12 @@ module Types | ||||||
|     field :pypi_package_requests_forwarding, GraphQL::Types::Boolean, |     field :pypi_package_requests_forwarding, GraphQL::Types::Boolean, | ||||||
|       null: true, |       null: true, | ||||||
|       description: 'Indicates whether PyPI package forwarding is allowed for this namespace.' |       description: 'Indicates whether PyPI package forwarding is allowed for this namespace.' | ||||||
|  |     field :terraform_module_duplicate_exception_regex, Types::UntrustedRegexp, | ||||||
|  |       null: true, | ||||||
|  |       description: 'When terraform_module_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.' | ||||||
|  |     field :terraform_module_duplicates_allowed, GraphQL::Types::Boolean, | ||||||
|  |       null: false, | ||||||
|  |       description: 'Indicates whether duplicate Terraform packages are allowed for this namespace.' | ||||||
| 
 | 
 | ||||||
|     field :lock_maven_package_requests_forwarding, GraphQL::Types::Boolean, |     field :lock_maven_package_requests_forwarding, GraphQL::Types::Boolean, | ||||||
|       null: false, |       null: false, | ||||||
|  |  | ||||||
|  | @ -1,14 +1,6 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| module EnvironmentHelper | module EnvironmentHelper | ||||||
|   # rubocop: disable CodeReuse/ActiveRecord |  | ||||||
|   def environment_for_build(project, build) |  | ||||||
|     return unless build.environment |  | ||||||
| 
 |  | ||||||
|     project.environments.find_by(name: build.expanded_environment_name) |  | ||||||
|   end |  | ||||||
|   # rubocop: enable CodeReuse/ActiveRecord |  | ||||||
| 
 |  | ||||||
|   def deployment_path(deployment) |   def deployment_path(deployment) | ||||||
|     [deployment.project, deployment.deployable] |     [deployment.project, deployment.deployable] | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ class Namespace::PackageSetting < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   PackageSettingNotImplemented = Class.new(StandardError) |   PackageSettingNotImplemented = Class.new(StandardError) | ||||||
| 
 | 
 | ||||||
|   PACKAGES_WITH_SETTINGS = %w[maven generic nuget].freeze |   PACKAGES_WITH_SETTINGS = %w[maven generic nuget terraform_module].freeze | ||||||
| 
 | 
 | ||||||
|   belongs_to :namespace, inverse_of: :package_setting_relation |   belongs_to :namespace, inverse_of: :package_setting_relation | ||||||
| 
 | 
 | ||||||
|  | @ -24,6 +24,14 @@ class Namespace::PackageSetting < ApplicationRecord | ||||||
|   validates :nuget_duplicates_allowed, inclusion: { in: [true, false] } |   validates :nuget_duplicates_allowed, inclusion: { in: [true, false] } | ||||||
|   validates :nuget_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } |   validates :nuget_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } | ||||||
|   validates :nuget_symbol_server_enabled, inclusion: { in: [true, false] } |   validates :nuget_symbol_server_enabled, inclusion: { in: [true, false] } | ||||||
|  |   validates :terraform_module_duplicates_allowed, inclusion: { in: [true, false] } | ||||||
|  |   validates :terraform_module_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } | ||||||
|  | 
 | ||||||
|  |   scope :namespace_id_in, ->(namespace_ids) { where(namespace_id: namespace_ids) } | ||||||
|  |   scope :with_terraform_module_duplicates_allowed_or_exception_regex, -> do | ||||||
|  |     where(terraform_module_duplicates_allowed: true) | ||||||
|  |       .or(where.not(terraform_module_duplicate_exception_regex: '')) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   class << self |   class << self | ||||||
|     def duplicates_allowed?(package) |     def duplicates_allowed?(package) | ||||||
|  |  | ||||||
|  | @ -13,12 +13,14 @@ module Organizations | ||||||
| 
 | 
 | ||||||
|     rule { admin }.policy do |     rule { admin }.policy do | ||||||
|       enable :admin_organization |       enable :admin_organization | ||||||
|  |       enable :create_group | ||||||
|       enable :read_organization |       enable :read_organization | ||||||
|       enable :read_organization_user |       enable :read_organization_user | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     rule { organization_user }.policy do |     rule { organization_user }.policy do | ||||||
|       enable :admin_organization |       enable :admin_organization | ||||||
|  |       enable :create_group | ||||||
|       enable :read_organization |       enable :read_organization | ||||||
|       enable :read_organization_user |       enable :read_organization_user | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -92,9 +92,32 @@ module Groups | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       unless organization_setting_valid? | ||||||
|  |         # We are unsetting this here to match behavior of invalid parent_id above and protect against possible | ||||||
|  |         # committing to the database of a value that isn't allowed. | ||||||
|  |         @group.organization = nil | ||||||
|  |         message = s_("CreateGroup|You don't have permission to create a group in the provided organization.") | ||||||
|  |         @group.errors.add(:organization_id, message) | ||||||
|  | 
 | ||||||
|  |         return false | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       true |       true | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def organization_setting_valid? | ||||||
|  |       # we check for the params presence explicitly since: | ||||||
|  |       # 1. We have a default organization_id at db level set and organization exists and may not have the entry | ||||||
|  |       #    in organization_users table to allow authorization. This shouldn't be the case longterm as we | ||||||
|  |       #    plan on populating organization_users correctly. | ||||||
|  |       # 2. We shouldn't need to check if this is allowed if the user didn't try to set it themselves. i.e. | ||||||
|  |       #    provided in the params | ||||||
|  |       return true if params[:organization_id].blank? | ||||||
|  |       return true if @group.organization.blank? | ||||||
|  | 
 | ||||||
|  |       can?(current_user, :create_group, @group.organization) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def can_use_visibility_level? |     def can_use_visibility_level? | ||||||
|       unless Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level) |       unless Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level) | ||||||
|         deny_visibility_level(@group) |         deny_visibility_level(@group) | ||||||
|  |  | ||||||
|  | @ -12,6 +12,8 @@ module Namespaces | ||||||
|                               maven_package_requests_forwarding |                               maven_package_requests_forwarding | ||||||
|                               nuget_duplicates_allowed |                               nuget_duplicates_allowed | ||||||
|                               nuget_duplicate_exception_regex |                               nuget_duplicate_exception_regex | ||||||
|  |                               terraform_module_duplicates_allowed | ||||||
|  |                               terraform_module_duplicate_exception_regex | ||||||
|                               npm_package_requests_forwarding |                               npm_package_requests_forwarding | ||||||
|                               pypi_package_requests_forwarding |                               pypi_package_requests_forwarding | ||||||
|                               lock_maven_package_requests_forwarding |                               lock_maven_package_requests_forwarding | ||||||
|  |  | ||||||
|  | @ -6,10 +6,20 @@ module Packages | ||||||
|       include Gitlab::Utils::StrongMemoize |       include Gitlab::Utils::StrongMemoize | ||||||
| 
 | 
 | ||||||
|       def execute |       def execute | ||||||
|         return error('Version is empty.', 400) if params[:module_version].blank? |         if params[:module_version].blank? | ||||||
|         return error('Access Denied', 403) if current_package_exists_elsewhere? |           return ServiceResponse.error(message: 'Version is empty.', reason: :bad_request) | ||||||
|         return error('Package version already exists.', 403) if current_package_version_exists? |         end | ||||||
|         return error('File is too large.', 400) if file_size_exceeded? | 
 | ||||||
|  |         if duplicates_not_allowed? && current_package_exists_elsewhere? | ||||||
|  |           return ServiceResponse.error( | ||||||
|  |             message: 'A package with the same name already exists in the namespace', | ||||||
|  |             reason: :forbidden | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         if current_package_version_exists? | ||||||
|  |           return ServiceResponse.error(message: 'Package version already exists.', reason: :forbidden) | ||||||
|  |         end | ||||||
| 
 | 
 | ||||||
|         ApplicationRecord.transaction { create_terraform_module_package! } |         ApplicationRecord.transaction { create_terraform_module_package! } | ||||||
|       end |       end | ||||||
|  | @ -24,6 +34,15 @@ module Packages | ||||||
|         package |         package | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       def duplicates_not_allowed? | ||||||
|  |         return true if package_settings_with_duplicates_allowed.blank? | ||||||
|  | 
 | ||||||
|  |         package_settings_with_duplicates_allowed.none? do |setting| | ||||||
|  |           setting.terraform_module_duplicates_allowed || | ||||||
|  |             ::Gitlab::UntrustedRegexp.new("\\A#{setting.terraform_module_duplicate_exception_regex}\\z").match?(name) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       def current_package_exists_elsewhere? |       def current_package_exists_elsewhere? | ||||||
|         ::Packages::Package |         ::Packages::Package | ||||||
|           .for_projects(project.root_namespace.all_projects.id_not_in(project.id)) |           .for_projects(project.root_namespace.all_projects.id_not_in(project.id)) | ||||||
|  | @ -62,9 +81,13 @@ module Packages | ||||||
|         } |         } | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def file_size_exceeded? |       def package_settings_with_duplicates_allowed | ||||||
|         project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size) |         ::Namespace::PackageSetting | ||||||
|  |           .select(:terraform_module_duplicates_allowed, :terraform_module_duplicate_exception_regex) | ||||||
|  |           .namespace_id_in(project.namespace.self_and_ancestor_ids) | ||||||
|  |           .with_terraform_module_duplicates_allowed_or_exception_regex | ||||||
|       end |       end | ||||||
|  |       strong_memoize_attr :package_settings_with_duplicates_allowed | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddTerraformModuleDuplicatesAllowedToNamespacePackageSettings < Gitlab::Database::Migration[2.2] | ||||||
|  |   milestone '16.8' | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     with_lock_retries do | ||||||
|  |       add_column(:namespace_package_settings, | ||||||
|  |         :terraform_module_duplicates_allowed, | ||||||
|  |         :boolean, | ||||||
|  |         null: false, | ||||||
|  |         default: false, | ||||||
|  |         if_not_exists: true | ||||||
|  |       ) | ||||||
|  | 
 | ||||||
|  |       add_column(:namespace_package_settings, | ||||||
|  |         :terraform_module_duplicate_exception_regex, | ||||||
|  |         :text, | ||||||
|  |         null: false, | ||||||
|  |         default: '', | ||||||
|  |         if_not_exists: true | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     add_text_limit(:namespace_package_settings, :terraform_module_duplicate_exception_regex, 255) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     with_lock_retries do | ||||||
|  |       remove_column(:namespace_package_settings, :terraform_module_duplicates_allowed, if_exists: true) | ||||||
|  |       remove_column(:namespace_package_settings, :terraform_module_duplicate_exception_regex, if_exists: true) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -3,24 +3,10 @@ | ||||||
| class ChangeICodeReviewCreateMrKeysFromRedisHllToRedis < Gitlab::Database::Migration[2.2] | class ChangeICodeReviewCreateMrKeysFromRedisHllToRedis < Gitlab::Database::Migration[2.2] | ||||||
|   milestone '16.8' |   milestone '16.8' | ||||||
| 
 | 
 | ||||||
|   disable_ddl_transaction! |  | ||||||
|   restrict_gitlab_migration gitlab_schema: :gitlab_main |  | ||||||
| 
 |  | ||||||
|   REDIS_HLL_PREFIX = '{hll_counters}_i_code_review_create_mr' |  | ||||||
|   REDIS_PREFIX = '{event_counters}_i_code_review_user_create_mr' |  | ||||||
| 
 |  | ||||||
|   def up |   def up | ||||||
|     # For each old (redis_hll) counter we find the corresponding target (redis) counter and add |     # no-op | ||||||
|     # old value to migrate a metric. If the Redis counter does not exist, it will get created. |     # | ||||||
|     # Since the RedisHLL keys expire after 6 weeks, we will migrate 6 keys at the most. |     # Removed due to https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17321 | ||||||
|     Gitlab::Redis::SharedState.with do |redis| |  | ||||||
|       redis.scan_each(match: "#{REDIS_HLL_PREFIX}-*") do |key| |  | ||||||
|         redis_key = key.sub(REDIS_HLL_PREFIX, REDIS_PREFIX) |  | ||||||
|         redis_hll_value = redis.pfcount(key) |  | ||||||
| 
 |  | ||||||
|         redis.incrby(redis_key, redis_hll_value) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def down |   def down | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | a18e718e99c23ae6db929929a905af0db72e3a3734d3c33e12ec2cdb44467f6d | ||||||
|  | @ -19505,9 +19505,12 @@ CREATE TABLE namespace_package_settings ( | ||||||
|     nuget_duplicates_allowed boolean DEFAULT true NOT NULL, |     nuget_duplicates_allowed boolean DEFAULT true NOT NULL, | ||||||
|     nuget_duplicate_exception_regex text DEFAULT ''::text NOT NULL, |     nuget_duplicate_exception_regex text DEFAULT ''::text NOT NULL, | ||||||
|     nuget_symbol_server_enabled boolean DEFAULT false NOT NULL, |     nuget_symbol_server_enabled boolean DEFAULT false NOT NULL, | ||||||
|  |     terraform_module_duplicates_allowed boolean DEFAULT false NOT NULL, | ||||||
|  |     terraform_module_duplicate_exception_regex text DEFAULT ''::text NOT NULL, | ||||||
|     CONSTRAINT check_31340211b1 CHECK ((char_length(generic_duplicate_exception_regex) <= 255)), |     CONSTRAINT check_31340211b1 CHECK ((char_length(generic_duplicate_exception_regex) <= 255)), | ||||||
|     CONSTRAINT check_d63274b2b6 CHECK ((char_length(maven_duplicate_exception_regex) <= 255)), |     CONSTRAINT check_d63274b2b6 CHECK ((char_length(maven_duplicate_exception_regex) <= 255)), | ||||||
|     CONSTRAINT check_eedcf85c48 CHECK ((char_length(nuget_duplicate_exception_regex) <= 255)) |     CONSTRAINT check_eedcf85c48 CHECK ((char_length(nuget_duplicate_exception_regex) <= 255)), | ||||||
|  |     CONSTRAINT check_f10503f1ad CHECK ((char_length(terraform_module_duplicate_exception_regex) <= 255)) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE namespace_root_storage_statistics ( | CREATE TABLE namespace_root_storage_statistics ( | ||||||
|  |  | ||||||
|  | @ -7788,6 +7788,8 @@ Input type: `UpdateNamespacePackageSettingsInput` | ||||||
| | <a id="mutationupdatenamespacepackagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. | | | <a id="mutationupdatenamespacepackagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. | | ||||||
| | <a id="mutationupdatenamespacepackagesettingsnugetsymbolserverenabled"></a>`nugetSymbolServerEnabled` | [`Boolean`](#boolean) | Indicates wheather the NuGet symbol server is enabled for this namespace. | | | <a id="mutationupdatenamespacepackagesettingsnugetsymbolserverenabled"></a>`nugetSymbolServerEnabled` | [`Boolean`](#boolean) | Indicates wheather the NuGet symbol server is enabled for this namespace. | | ||||||
| | <a id="mutationupdatenamespacepackagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. | | | <a id="mutationupdatenamespacepackagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. | | ||||||
|  | | <a id="mutationupdatenamespacepackagesettingsterraformmoduleduplicateexceptionregex"></a>`terraformModuleDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When terraform_module_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. | | ||||||
|  | | <a id="mutationupdatenamespacepackagesettingsterraformmoduleduplicatesallowed"></a>`terraformModuleDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate Terraform packages are allowed for this namespace. | | ||||||
| 
 | 
 | ||||||
| #### Fields | #### Fields | ||||||
| 
 | 
 | ||||||
|  | @ -23730,6 +23732,8 @@ Namespace-level Package Registry settings. | ||||||
| | <a id="packagesettingsnugetsymbolserverenabled"></a>`nugetSymbolServerEnabled` | [`Boolean!`](#boolean) | Indicates wheather the NuGet symbol server is enabled for this namespace. | | | <a id="packagesettingsnugetsymbolserverenabled"></a>`nugetSymbolServerEnabled` | [`Boolean!`](#boolean) | Indicates wheather the NuGet symbol server is enabled for this namespace. | | ||||||
| | <a id="packagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. | | | <a id="packagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. | | ||||||
| | <a id="packagesettingspypipackagerequestsforwardinglocked"></a>`pypiPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether PyPI package forwarding settings are locked by a parent namespace. | | | <a id="packagesettingspypipackagerequestsforwardinglocked"></a>`pypiPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether PyPI package forwarding settings are locked by a parent namespace. | | ||||||
|  | | <a id="packagesettingsterraformmoduleduplicateexceptionregex"></a>`terraformModuleDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When terraform_module_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. | | ||||||
|  | | <a id="packagesettingsterraformmoduleduplicatesallowed"></a>`terraformModuleDuplicatesAllowed` | [`Boolean!`](#boolean) | Indicates whether duplicate Terraform packages are allowed for this namespace. | | ||||||
| 
 | 
 | ||||||
| ### `PackageTag` | ### `PackageTag` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -816,31 +816,32 @@ POST /groups | ||||||
| 
 | 
 | ||||||
| Parameters: | Parameters: | ||||||
| 
 | 
 | ||||||
| | Attribute                                               | Type    | Required | Description | | | Attribute                                               | Type    | Required | Description                                                                                                                                                                                     | | ||||||
| | ------------------------------------------------------- | ------- | -------- | ----------- | | | ------------------------------------------------------- | ------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||||
| | `name`                                                  | string  | yes      | The name of the group. | | | `name`                                                  | string  | yes      | The name of the group.                                                                                                                                                                          | | ||||||
| | `path`                                                  | string  | yes      | The path of the group. | | | `path`                                                  | string  | yes      | The path of the group.                                                                                                                                                                          | | ||||||
| | `auto_devops_enabled`                                   | boolean | no       | Default to Auto DevOps pipeline for all projects within this group. | | | `auto_devops_enabled`                                   | boolean | no       | Default to Auto DevOps pipeline for all projects within this group.                                                                                                                             | | ||||||
| | `avatar`                                                | mixed   | no       | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) | | | `avatar`                                                | mixed   | no       | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681)                                                                            | | ||||||
| | `default_branch_protection`                             | integer | no       | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. | | | `default_branch_protection`                             | integer | no       | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting.                                           | | ||||||
| | `default_branch_protection_defaults`                    | hash    | no       | See [Options for `default_branch_protection_defaults`](#options-for-default_branch_protection_defaults). | | | `default_branch_protection_defaults`                    | hash    | no       | See [Options for `default_branch_protection_defaults`](#options-for-default_branch_protection_defaults).                                                                                        | | ||||||
| | `description`                                           | string  | no       | The group's description. | | | `description`                                           | string  | no       | The group's description.                                                                                                                                                                        | | ||||||
| | `emails_disabled`                                       | boolean | no       | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead. | | | `emails_disabled`                                       | boolean | no       | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead.                                       | | ||||||
| | `emails_enabled`                                        | boolean | no       | Enable email notifications. | | | `emails_enabled`                                        | boolean | no       | Enable email notifications.                                                                                                                                                                     | | ||||||
| | `lfs_enabled`                                           | boolean | no       | Enable/disable Large File Storage (LFS) for the projects in this group. | | | `lfs_enabled`                                           | boolean | no       | Enable/disable Large File Storage (LFS) for the projects in this group.                                                                                                                         | | ||||||
| | `mentions_disabled`                                     | boolean | no       | Disable the capability of a group from getting mentioned. | | | `mentions_disabled`                                     | boolean | no       | Disable the capability of a group from getting mentioned.                                                                                                                                       | | ||||||
| | `parent_id`                                             | integer | no       | The parent group ID for creating nested group. | | | `organization_id`                                       | integer | no       | The organization ID for the group.                                                                                                                                                              | | ||||||
|  | | `parent_id`                                             | integer | no       | The parent group ID for creating nested group.                                                                                                                                                  | | ||||||
| | `project_creation_level`                                | string  | no       | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). | | | `project_creation_level`                                | string  | no       | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). | | ||||||
| | `request_access_enabled`                                | boolean | no       | Allow users to request member access. | | | `request_access_enabled`                                | boolean | no       | Allow users to request member access.                                                                                                                                                           | | ||||||
| | `require_two_factor_authentication`                     | boolean | no       | Require all users in this group to setup Two-factor authentication. | | | `require_two_factor_authentication`                     | boolean | no       | Require all users in this group to setup Two-factor authentication.                                                                                                                             | | ||||||
| | `share_with_group_lock`                                 | boolean | no       | Prevent sharing a project with another group within this group. | | | `share_with_group_lock`                                 | boolean | no       | Prevent sharing a project with another group within this group.                                                                                                                                 | | ||||||
| | `subgroup_creation_level`                               | string  | no       | Allowed to [create subgroups](../user/group/subgroups/index.md#create-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). | | | `subgroup_creation_level`                               | string  | no       | Allowed to [create subgroups](../user/group/subgroups/index.md#create-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role).                                   | | ||||||
| | `two_factor_grace_period`                               | integer | no       | Time before Two-factor authentication is enforced (in hours). | | | `two_factor_grace_period`                               | integer | no       | Time before Two-factor authentication is enforced (in hours).                                                                                                                                   | | ||||||
| | `visibility`                                            | string  | no       | The group's visibility. Can be `private`, `internal`, or `public`. | | | `visibility`                                            | string  | no       | The group's visibility. Can be `private`, `internal`, or `public`.                                                                                                                              | | ||||||
| | `membership_lock` **(PREMIUM ALL)**                     | boolean | no       | Users cannot be added to projects in this group. | | | `membership_lock` **(PREMIUM ALL)**                     | boolean | no       | Users cannot be added to projects in this group.                                                                                                                                                | | ||||||
| | `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no       | Can be set by administrators only. Additional compute minutes for this group. | | | `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no       | Can be set by administrators only. Additional compute minutes for this group.                                                                                                                   | | ||||||
| | `shared_runners_minutes_limit` **(PREMIUM SELF)**       | integer | no       | Can be set by administrators only. Maximum number of monthly compute minutes for this group. Can be `nil` (default; inherit system default), `0` (unlimited), or `> 0`. | | | `shared_runners_minutes_limit` **(PREMIUM SELF)**       | integer | no       | Can be set by administrators only. Maximum number of monthly compute minutes for this group. Can be `nil` (default; inherit system default), `0` (unlimited), or `> 0`.                         | | ||||||
| | `wiki_access_level` **(PREMIUM ALL)**                       | string  | no       | The wiki access level. Can be `disabled`, `private`, or `enabled`. | | | `wiki_access_level` **(PREMIUM ALL)**                       | string  | no       | The wiki access level. Can be `disabled`, `private`, or `enabled`.                                                                                                                              | | ||||||
| 
 | 
 | ||||||
| ### Options for `default_branch_protection` | ### Options for `default_branch_protection` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -70,6 +70,8 @@ Setting | Table | Description | ||||||
| `nuget_duplicates_allowed` | `namespace_package_settings` | Allow or prevent duplicate NuGet packages. | `nuget_duplicates_allowed` | `namespace_package_settings` | Allow or prevent duplicate NuGet packages. | ||||||
| `nuget_duplicate_exception_regex` | `namespace_package_settings` | Regex defining NuGet packages that are allowed to be duplicate when duplicates are not allowed. | `nuget_duplicate_exception_regex` | `namespace_package_settings` | Regex defining NuGet packages that are allowed to be duplicate when duplicates are not allowed. | ||||||
| `nuget_symbol_server_enabled` | `namespace_package_settings` | Enable or disable the NuGet symbol server. | `nuget_symbol_server_enabled` | `namespace_package_settings` | Enable or disable the NuGet symbol server. | ||||||
|  | `terraform_module_duplicates_allowed` | `namespace_package_settings` | Allow or prevent duplicate Terraform module packages. | ||||||
|  | `terraform_module_duplicate_exception_regex` | `namespace_package_settings` | Regex defining Terraform module packages that are allowed to be duplicate when duplicates are not allowed. | ||||||
| Dependency Proxy Cleanup Policies - `ttl` | `dependency_proxy_image_ttl_group_policies` | Number of days to retain an unused Dependency Proxy file before it is removed. | Dependency Proxy Cleanup Policies - `ttl` | `dependency_proxy_image_ttl_group_policies` | Number of days to retain an unused Dependency Proxy file before it is removed. | ||||||
| Dependency Proxy - `enabled` | `dependency_proxy_image_ttl_group_policies` | Enable or disable the Dependency Proxy cleanup policy. | Dependency Proxy - `enabled` | `dependency_proxy_image_ttl_group_policies` | Enable or disable the Dependency Proxy cleanup policy. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -48,10 +48,10 @@ You can publish Terraform modules by using the [Terraform Module Registry API](. | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
| - The package name and version [must be unique in the top-level namespace](#how-module-resolution-works). | - Unless [duplicates are allowed](#allow-duplicate-terraform-modules), the package name and version [must be unique in the top-level namespace](#how-module-resolution-works). | ||||||
| - Your project and group names must not include a dot (`.`). For example, `source = "gitlab.example.com/my.group/project.name"`. | - Your project and group names must not include a dot (`.`). For example, `source = "gitlab.example.com/my.group/project.name"`. | ||||||
| - You must [authenticate with the API](../../../api/rest/index.md#authentication). If authenticating with a deploy token, it must be configured with the `write_package_registry` scope. | - You must [authenticate with the API](../../../api/rest/index.md#authentication). If authenticating with a deploy token, it must be configured with the `write_package_registry` scope. | ||||||
| - The name of a module [must be unique in the scope of its group](#how-module-resolution-works), otherwise an | - Unless [duplicates are allowed](#allow-duplicate-terraform-modules), the name of a module [must be unique in the scope of its group](#how-module-resolution-works), otherwise an | ||||||
|   [error occurs](#troubleshooting). |   [error occurs](#troubleshooting). | ||||||
| 
 | 
 | ||||||
| ```plaintext | ```plaintext | ||||||
|  | @ -157,6 +157,22 @@ upload: | ||||||
| To trigger this upload job, add a Git tag to your commit. Ensure the tag follows the [Semantic versioning specification](https://semver.org/) that Terraform requires. The `rules:if: $CI_COMMIT_TAG` ensures that only tagged commits to your repository trigger the module upload job. | To trigger this upload job, add a Git tag to your commit. Ensure the tag follows the [Semantic versioning specification](https://semver.org/) that Terraform requires. The `rules:if: $CI_COMMIT_TAG` ensures that only tagged commits to your repository trigger the module upload job. | ||||||
| For other ways to control jobs in your CI/CD pipeline, refer to the [`.gitlab-ci.yml`](../../../ci/yaml/index.md) keyword reference. | For other ways to control jobs in your CI/CD pipeline, refer to the [`.gitlab-ci.yml`](../../../ci/yaml/index.md) keyword reference. | ||||||
| 
 | 
 | ||||||
|  | ### Allow duplicate Terraform modules | ||||||
|  | 
 | ||||||
|  | > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/368040) in GitLab 16.8. | ||||||
|  | 
 | ||||||
|  | By default, the Terraform Module Registry enforces uniqueness for module names in the same namespace. To allow publishing duplicate module names: | ||||||
|  | 
 | ||||||
|  | - Enable `terraform_module_duplicates_allowed` for the namespace with the [GraphQl API](../../../api/graphql/reference/index.md#packagesettings). | ||||||
|  | 
 | ||||||
|  | To allow duplicates with specific names: | ||||||
|  | 
 | ||||||
|  | 1. Ensure `terraform_module_duplicates_allowed` is disabled. | ||||||
|  | 1. Use `terraform_module_duplicate_exception_regex` to define a regex pattern for the module names you want to allow duplicates for. | ||||||
|  | 
 | ||||||
|  | The top-level namespace setting takes precedence over the child namespace settings. | ||||||
|  | For example, if you enable `terraform_module_duplicates_allowed` for a group, and disable it for a subgroup, duplicates are allowed for all projects in the group and its subgroups. | ||||||
|  | 
 | ||||||
| ## Reference a Terraform module | ## Reference a Terraform module | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
|  | @ -209,6 +225,8 @@ module "<module>" { | ||||||
| 
 | 
 | ||||||
| If you need to reference the latest version of a module, you can omit the `<module-version>` from the source URL. To prevent future issues, you should reference a specific version if possible. | If you need to reference the latest version of a module, you can omit the `<module-version>` from the source URL. To prevent future issues, you should reference a specific version if possible. | ||||||
| 
 | 
 | ||||||
|  | If there are [duplicate module names](#allow-duplicate-terraform-modules) in the same namespace, referencing the module from the namespace level installs the recently published module. To reference a specific version of a duplicate module, use the [project-level](#from-a-project) source type. | ||||||
|  | 
 | ||||||
| ## Download a Terraform module | ## Download a Terraform module | ||||||
| 
 | 
 | ||||||
| To download a Terraform module: | To download a Terraform module: | ||||||
|  | @ -275,4 +293,4 @@ For examples of the Terraform Module Registry, check the projects below: | ||||||
| 
 | 
 | ||||||
| ## Troubleshooting | ## Troubleshooting | ||||||
| 
 | 
 | ||||||
| - Publishing a module with a duplicate name results in a `{"message":"Access Denied"}` error. There's an ongoing discussion about allowing duplicate module names [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/368040). | - Publishing a module with a duplicate name results in a `{"message":"A package with the same name already exists in the namespace"}` error. | ||||||
|  |  | ||||||
|  | @ -395,6 +395,6 @@ third-party Git clients. | ||||||
| 
 | 
 | ||||||
| ### Branch names are case-sensitive | ### Branch names are case-sensitive | ||||||
| 
 | 
 | ||||||
| Branch names in `git` are case-sensitive. When configuring your protected branch | Branch names in `git` are case-sensitive. When configuring your protected branch, | ||||||
| or [target branch rule](repository/branches/index.md#configure-rules-for-target-branches), | or your [target branch workflow](repository/branches/index.md#configure-workflows-for-target-branches), | ||||||
| `dev` is not the same `DEV` or `Dev`. | `dev` is not the same `DEV` or `Dev`. | ||||||
|  |  | ||||||
|  | @ -287,58 +287,58 @@ To do this: | ||||||
| 1. Select **Delete merged branches**. | 1. Select **Delete merged branches**. | ||||||
| 1. In the dialog, enter the word `delete` to confirm, then select **Delete merged branches**. | 1. In the dialog, enter the word `delete` to confirm, then select **Delete merged branches**. | ||||||
| 
 | 
 | ||||||
| ## Configure rules for target branches **(PREMIUM ALL)** | ## Configure workflows for target branches **(PREMIUM ALL)** | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127115) in GitLab 16.4 [with a flag](../../../../administration/feature_flags.md) named `target_branch_rules_flag`. Enabled by default. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127115) in GitLab 16.4 [with a flag](../../../../administration/feature_flags.md) named `target_branch_rules_flag`. Enabled by default. | ||||||
| > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136431) in GitLab 16.7. | > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136431) in GitLab 16.7. | ||||||
| 
 | 
 | ||||||
| Some projects use multiple long-term branches for development, like `develop` and `qa`. | Some projects use multiple long-term branches for development, like `develop` and `qa`. | ||||||
| In these projects, you might want to keep `main` as the default branch, but expect | In these projects, you might want to keep `main` as the default branch, but expect | ||||||
| merge requests to target `develop` or `qa` instead. Target branch rules help ensure | merge requests to target `develop` or `qa` instead. Target branch workflows help ensure | ||||||
| merge requests target the appropriate development branch for your project. | merge requests target the appropriate development branch for your project. | ||||||
| 
 | 
 | ||||||
| When you create a merge request, the rule checks the name of the branch. If the | When you create a merge request, the workflow checks the name of the branch. If the | ||||||
| branch name matches the rule, the merge request targets the branch you specify | branch name matches the workflow, the merge request targets the branch you specify. If the branch name does not match, the merge request targets the | ||||||
| in the rule. If the branch name does not match, the merge request targets the |  | ||||||
| default branch of the project. | default branch of the project. | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
| - You must have at least the Maintainer role. | - You must have at least the Maintainer role. | ||||||
| 
 | 
 | ||||||
| To create a target branch rule: | To create a target branch workflow: | ||||||
| 
 | 
 | ||||||
| 1. On the left sidebar, select **Search or go to** and find your project. | 1. On the left sidebar, select **Search or go to** and find your project. | ||||||
| 1. Select **Settings > Merge requests**. | 1. Select **Settings > Merge requests**. | ||||||
| 1. Select **Add target branch rule**. | 1. Scroll down to **Merge request branch workflow** | ||||||
| 1. For **Rule name**, provide a string or wild card to compare against branch names. | 1. Select **Add branch target**. | ||||||
| 1. Select the **Target branch** to use when the branch name matches the **Rule name**. | 1. For **Branch name pattern**, provide a string or wild card to compare against branch names. | ||||||
|  | 1. Select the **Target branch** to use when the branch name matches the **Branch name pattern**. | ||||||
| 1. Select **Save**. | 1. Select **Save**. | ||||||
| 
 | 
 | ||||||
| ### Example | ### Example | ||||||
| 
 | 
 | ||||||
| You could configure your project to have the following target branch rules: | You could configure your project to have the following target branch workflows: | ||||||
| 
 | 
 | ||||||
| | Rule name   | Target branch | | | Branch name pattern   | Target branch | | ||||||
| |-------------|---------------| | |-------------|---------------| | ||||||
| | `feature/*` | `develop`     | | | `feature/*` | `develop`     | | ||||||
| | `bug/*`     | `develop`     | | | `bug/*`     | `develop`     | | ||||||
| | `release/*` | `main`        | | | `release/*` | `main`        | | ||||||
| 
 | 
 | ||||||
| These rules simplify the process of creating merge requests for a project that: | These target branches simplify the process of creating merge requests for a project that: | ||||||
| 
 | 
 | ||||||
| - Uses `main` to represent the deployed state of your application. | - Uses `main` to represent the deployed state of your application. | ||||||
| - Tracks current, unreleased development work in another long-running branch, like `develop`. | - Tracks current, unreleased development work in another long-running branch, like `develop`. | ||||||
| 
 | 
 | ||||||
| If your workflow initially places new features in `develop` instead of `main`, these rules | If your workflow initially places new features in `develop` instead of `main`, these target branches | ||||||
| ensure all branches matching either `feature/*` or `bug/*` do not target `main` by mistake. | ensure all branches matching either `feature/*` or `bug/*` do not target `main` by mistake. | ||||||
| 
 | 
 | ||||||
| When you're ready to release to `main`, create a branch named `release/*`, and the rules | When you're ready to release to `main`, create a branch named `release/*`, and | ||||||
| ensure this branch targets `main`. | ensure this branch targets `main`. | ||||||
| 
 | 
 | ||||||
| ## Delete a target branch rule | ## Delete a target branch workflow | ||||||
| 
 | 
 | ||||||
| When you remove a target branch rule, existing merge requests remain unchanged. | When you remove a target branch workflow, existing merge requests remain unchanged. | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
|  | @ -348,7 +348,7 @@ To do this: | ||||||
| 
 | 
 | ||||||
| 1. On the left sidebar, select **Search or go to** and find your project. | 1. On the left sidebar, select **Search or go to** and find your project. | ||||||
| 1. Select **Settings > Merge requests**. | 1. Select **Settings > Merge requests**. | ||||||
| 1. Select **Delete** on the rule you want to delete. | 1. Select **Delete** on the branch target you want to delete. | ||||||
| 
 | 
 | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ module API | ||||||
|       expose :full_name, :full_path |       expose :full_name, :full_path | ||||||
|       expose :created_at |       expose :created_at | ||||||
|       expose :parent_id |       expose :parent_id | ||||||
|  |       expose :organization_id | ||||||
|       expose :shared_runners_setting |       expose :shared_runners_setting | ||||||
| 
 | 
 | ||||||
|       expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes |       expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes | ||||||
|  |  | ||||||
|  | @ -213,11 +213,15 @@ module API | ||||||
|         requires :name, type: String, desc: 'The name of the group' |         requires :name, type: String, desc: 'The name of the group' | ||||||
|         requires :path, type: String, desc: 'The path of the group' |         requires :path, type: String, desc: 'The path of the group' | ||||||
|         optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' |         optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' | ||||||
|  |         optional :organization_id, type: Integer, desc: 'The organization id for the group' | ||||||
| 
 | 
 | ||||||
|         use :optional_params |         use :optional_params | ||||||
|       end |       end | ||||||
|       post feature_category: :groups_and_projects, urgency: :low do |       post feature_category: :groups_and_projects, urgency: :low do | ||||||
|         parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? |         organization = find_organization!(params[:organization_id]) if params[:organization_id].present? | ||||||
|  |         authorize! :create_group, organization if organization | ||||||
|  | 
 | ||||||
|  |         parent_group = find_group!(params[:parent_id], organization: organization) if params[:parent_id].present? | ||||||
|         if parent_group |         if parent_group | ||||||
|           authorize! :create_subgroup, parent_group |           authorize! :create_subgroup, parent_group | ||||||
|         else |         else | ||||||
|  |  | ||||||
|  | @ -211,18 +211,25 @@ module API | ||||||
|       not_found!('Pipeline') |       not_found!('Pipeline') | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def find_organization!(id) | ||||||
|  |       organization = Organizations::Organization.find_by_id(id) | ||||||
|  |       check_organization_access(organization) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     # rubocop: disable CodeReuse/ActiveRecord |     # rubocop: disable CodeReuse/ActiveRecord | ||||||
|     def find_group(id) |     def find_group(id, organization: nil) | ||||||
|  |       collection = organization.present? ? Group.in_organization(organization) : Group.all | ||||||
|  | 
 | ||||||
|       if id.to_s =~ INTEGER_ID_REGEX |       if id.to_s =~ INTEGER_ID_REGEX | ||||||
|         Group.find_by(id: id) |         collection.find_by(id: id) | ||||||
|       else |       else | ||||||
|         Group.find_by_full_path(id) |         collection.find_by_full_path(id) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|     # rubocop: enable CodeReuse/ActiveRecord |     # rubocop: enable CodeReuse/ActiveRecord | ||||||
| 
 | 
 | ||||||
|     def find_group!(id) |     def find_group!(id, organization: nil) | ||||||
|       group = find_group(id) |       group = find_group(id, organization: organization) | ||||||
|       check_group_access(group) |       check_group_access(group) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -835,6 +842,12 @@ module API | ||||||
|       @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] |       @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def check_organization_access(organization) | ||||||
|  |       return organization if can?(current_user, :read_organization, organization) | ||||||
|  | 
 | ||||||
|  |       not_found!('Organization') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def secret_token |     def secret_token | ||||||
|       Gitlab::Shell.secret_token |       Gitlab::Shell.secret_token | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -171,7 +171,7 @@ module API | ||||||
|                       .new(authorized_user_project, current_user, create_package_file_params) |                       .new(authorized_user_project, current_user, create_package_file_params) | ||||||
|                       .execute |                       .execute | ||||||
| 
 | 
 | ||||||
|                     render_api_error!(result[:message], result[:http_status]) if result[:status] == :error |                     render_api_error!(result.message, result.reason) if result.error? | ||||||
| 
 | 
 | ||||||
|                     track_package_event('push_package', :terraform_module, project: authorized_user_project, |                     track_package_event('push_package', :terraform_module, project: authorized_user_project, | ||||||
|                       namespace: authorized_user_project.namespace) |                       namespace: authorized_user_project.namespace) | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ module Gitlab | ||||||
|       def gitlab_id |       def gitlab_id | ||||||
|         return @gitlab_id if defined?(@gitlab_id) |         return @gitlab_id if defined?(@gitlab_id) | ||||||
| 
 | 
 | ||||||
|         @gitlab_id = find_by_external_uid || find_by_email |         @gitlab_id = find_by_email | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       private |       private | ||||||
|  | @ -45,14 +45,6 @@ module Gitlab | ||||||
|         User.find_by_any_email(email) |         User.find_by_any_email(email) | ||||||
|             .try(:id) |             .try(:id) | ||||||
|       end |       end | ||||||
| 
 |  | ||||||
|       # rubocop: disable CodeReuse/ActiveRecord |  | ||||||
|       def find_by_external_uid |  | ||||||
|         return unless id |  | ||||||
| 
 |  | ||||||
|         User.by_provider_and_extern_uid(:github, id).select(:id).first&.id |  | ||||||
|       end |  | ||||||
|       # rubocop: enable CodeReuse/ActiveRecord |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -14578,6 +14578,9 @@ msgstr "" | ||||||
| msgid "CreateGitTag|Set tag message" | msgid "CreateGitTag|Set tag message" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "CreateGroup|You don't have permission to create a group in the provided organization." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "CreateGroup|You don’t have permission to create a subgroup in this group." | msgid "CreateGroup|You don’t have permission to create a subgroup in this group." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,6 +15,9 @@ FactoryBot.define do | ||||||
| 
 | 
 | ||||||
|     nuget_symbol_server_enabled { false } |     nuget_symbol_server_enabled { false } | ||||||
| 
 | 
 | ||||||
|  |     terraform_module_duplicates_allowed { false } | ||||||
|  |     terraform_module_duplicate_exception_regex { 'foo' } | ||||||
|  | 
 | ||||||
|     trait :group do |     trait :group do | ||||||
|       namespace { association(:group) } |       namespace { association(:group) } | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -122,6 +122,7 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => { | ||||||
|       [[{ value: '1' }]], |       [[{ value: '1' }]], | ||||||
|       [[{ value: 1, disabled: true }]], |       [[{ value: 1, disabled: true }]], | ||||||
|       [[{ value: true, disabled: false }]], |       [[{ value: true, disabled: false }]], | ||||||
|  |       [[{ value: true, props: { 'data-testid': 'test' } }]], | ||||||
|     ])('with options=%j, passes validation', (options) => { |     ])('with options=%j, passes validation', (options) => { | ||||||
|       createComponent({ options }); |       createComponent({ options }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,7 +39,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | ||||||
|           lock_npm_package_requests_forwarding: false, |           lock_npm_package_requests_forwarding: false, | ||||||
|           pypi_package_requests_forwarding: nil, |           pypi_package_requests_forwarding: nil, | ||||||
|           lock_pypi_package_requests_forwarding: false, |           lock_pypi_package_requests_forwarding: false, | ||||||
|           nuget_symbol_server_enabled: false |           nuget_symbol_server_enabled: false, | ||||||
|  |           terraform_module_duplicates_allowed: false, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'foo' | ||||||
|         }, to: { |         }, to: { | ||||||
|           maven_duplicates_allowed: false, |           maven_duplicates_allowed: false, | ||||||
|           maven_duplicate_exception_regex: 'RELEASE', |           maven_duplicate_exception_regex: 'RELEASE', | ||||||
|  | @ -53,7 +55,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | ||||||
|           lock_npm_package_requests_forwarding: true, |           lock_npm_package_requests_forwarding: true, | ||||||
|           pypi_package_requests_forwarding: true, |           pypi_package_requests_forwarding: true, | ||||||
|           lock_pypi_package_requests_forwarding: true, |           lock_pypi_package_requests_forwarding: true, | ||||||
|           nuget_symbol_server_enabled: true |           nuget_symbol_server_enabled: true, | ||||||
|  |           terraform_module_duplicates_allowed: true, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'bar' | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|       it_behaves_like 'returning a success' |       it_behaves_like 'returning a success' | ||||||
|  | @ -109,7 +113,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | ||||||
|           lock_npm_package_requests_forwarding: true, |           lock_npm_package_requests_forwarding: true, | ||||||
|           pypi_package_requests_forwarding: true, |           pypi_package_requests_forwarding: true, | ||||||
|           lock_pypi_package_requests_forwarding: true, |           lock_pypi_package_requests_forwarding: true, | ||||||
|           nuget_symbol_server_enabled: true |           nuget_symbol_server_enabled: true, | ||||||
|  |           terraform_module_duplicates_allowed: true, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'bar' | ||||||
|         } |         } | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -33,6 +33,8 @@ RSpec.describe GitlabSchema.types['PackageSettings'], feature_category: :package | ||||||
|       npm_package_requests_forwarding_locked |       npm_package_requests_forwarding_locked | ||||||
|       pypi_package_requests_forwarding_locked |       pypi_package_requests_forwarding_locked | ||||||
|       nuget_symbol_server_enabled |       nuget_symbol_server_enabled | ||||||
|  |       terraform_module_duplicates_allowed | ||||||
|  |       terraform_module_duplicate_exception_regex | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     expect(described_class).to include_graphql_fields(*expected_fields) |     expect(described_class).to include_graphql_fields(*expected_fields) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe API::Entities::Group, feature_category: :groups_and_projects do | ||||||
|  |   let_it_be(:group) do | ||||||
|  |     base_group = create(:group) { |g| create(:project_statistics, namespace_id: g.id) } | ||||||
|  |     Group.with_statistics.find(base_group.id) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject(:json) { described_class.new(group, { with_custom_attributes: true, statistics: true }).as_json } | ||||||
|  | 
 | ||||||
|  |   it 'returns expected data' do | ||||||
|  |     expect(json.keys).to( | ||||||
|  |       include( | ||||||
|  |         :organization_id, :path, :description, :visibility, :share_with_group_lock, :require_two_factor_authentication, | ||||||
|  |         :two_factor_grace_period, :project_creation_level, :auto_devops_enabled, | ||||||
|  |         :subgroup_creation_level, :emails_disabled, :emails_enabled, :lfs_enabled, :default_branch_protection, | ||||||
|  |         :default_branch_protection_defaults, :avatar_url, :request_access_enabled, :full_name, :full_path, :created_at, | ||||||
|  |         :parent_id, :organization_id, :shared_runners_setting, :custom_attributes, :statistics | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -406,6 +406,37 @@ RSpec.describe API::Helpers, feature_category: :shared do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '#find_organization!' do | ||||||
|  |     let_it_be(:organization) { create(:organization) } | ||||||
|  |     let_it_be(:user) { create(:user) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       allow(helper).to receive(:current_user).and_return(user) | ||||||
|  |       allow(helper).to receive(:initial_current_user).and_return(user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user is authenticated' do | ||||||
|  |       it 'returns requested organization' do | ||||||
|  |         expect(helper.find_organization!(organization.id)).to eq(organization) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user is not authenticated' do | ||||||
|  |       let(:user) { nil } | ||||||
|  | 
 | ||||||
|  |       it 'returns requested organization' do | ||||||
|  |         expect(helper.find_organization!(organization.id)).to eq(organization) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when organization does not exist' do | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(helper).to receive(:render_api_error!).with('404 Organization Not Found', 404) | ||||||
|  |         expect(helper.find_organization!(non_existing_record_id)).to be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#find_group!' do |   describe '#find_group!' do | ||||||
|     let_it_be(:group) { create(:group, :public) } |     let_it_be(:group) { create(:group, :public) } | ||||||
|     let_it_be(:user) { create(:user) } |     let_it_be(:user) { create(:user) } | ||||||
|  | @ -457,7 +488,7 @@ RSpec.describe API::Helpers, feature_category: :shared do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'support for IDs and paths as arguments' do |     context 'with support for IDs and paths as arguments' do | ||||||
|       let_it_be(:group) { create(:group) } |       let_it_be(:group) { create(:group) } | ||||||
| 
 | 
 | ||||||
|       let(:user) { group.first_owner } |       let(:user) { group.first_owner } | ||||||
|  | @ -505,6 +536,34 @@ RSpec.describe API::Helpers, feature_category: :shared do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   context 'with support for organization as an argument' do | ||||||
|  |     let_it_be(:group) { create(:group) } | ||||||
|  |     let_it_be(:organization) { create(:organization) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       allow(helper).to receive(:current_user).and_return(group.first_owner) | ||||||
|  |       allow(helper).to receive(:job_token_authentication?).and_return(false) | ||||||
|  |       allow(helper).to receive(:authenticate_non_public?).and_return(false) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     subject { helper.find_group!(group.id, organization: organization) } | ||||||
|  | 
 | ||||||
|  |     context 'when group exists in the organization' do | ||||||
|  |       before do | ||||||
|  |         group.update!(organization: organization) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to eq(group) } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when group does not exist in the organization' do | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(helper).to receive(:render_api_error!).with('404 Group Not Found', 404) | ||||||
|  |         is_expected.to be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#find_group_by_full_path!' do |   describe '#find_group_by_full_path!' do | ||||||
|     let_it_be(:group) { create(:group, :public) } |     let_it_be(:group) { create(:group, :public) } | ||||||
|     let_it_be(:user) { create(:user) } |     let_it_be(:user) { create(:user) } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Gitlab::LegacyGithubImport::CommentFormatter do | RSpec.describe Gitlab::LegacyGithubImport::CommentFormatter, feature_category: :importers do | ||||||
|   let_it_be(:project) { create(:project) } |   let_it_be(:project) { create(:project) } | ||||||
|   let(:client) { double } |   let(:client) { double } | ||||||
|   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } |   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } | ||||||
|  | @ -76,12 +76,6 @@ RSpec.describe Gitlab::LegacyGithubImport::CommentFormatter do | ||||||
|     context 'when author is a GitLab user' do |     context 'when author is a GitLab user' do | ||||||
|       let(:raw) { base.merge(user: octocat) } |       let(:raw) { base.merge(user: octocat) } | ||||||
| 
 | 
 | ||||||
|       it 'returns GitLab user id associated with GitHub id as author_id' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(comment.attributes.fetch(:author_id)).to eq gl_user.id |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id associated with GitHub email as author_id' do |       it 'returns GitLab user id associated with GitHub email as author_id' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  | @ -89,7 +83,7 @@ RSpec.describe Gitlab::LegacyGithubImport::CommentFormatter do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns note without created at tag line' do |       it 'returns note without created at tag line' do | ||||||
|         create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |         create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|         expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.") |         expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.") | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do | RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter, feature_category: :importers do | ||||||
|   let_it_be(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } |   let_it_be(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } | ||||||
|   let(:client) { double } |   let(:client) { double } | ||||||
|   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } |   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } | ||||||
|  | @ -82,12 +82,6 @@ RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do | ||||||
|         expect(issue.attributes.fetch(:assignee_ids)).to be_empty |         expect(issue.attributes.fetch(:assignee_ids)).to be_empty | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns GitLab user id associated with GitHub id as assignee_id' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id] |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id associated with GitHub email as assignee_id' do |       it 'returns GitLab user id associated with GitHub email as assignee_id' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  | @ -117,12 +111,6 @@ RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do | ||||||
|         expect(issue.attributes.fetch(:author_id)).to eq project.creator_id |         expect(issue.attributes.fetch(:author_id)).to eq project.creator_id | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns GitLab user id associated with GitHub id as author_id' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(issue.attributes.fetch(:author_id)).to eq gl_user.id |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id associated with GitHub email as author_id' do |       it 'returns GitLab user id associated with GitHub email as author_id' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  | @ -130,7 +118,7 @@ RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns description without created at tag line' do |       it 'returns description without created at tag line' do | ||||||
|         create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |         create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|         expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.") |         expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.") | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do | RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter, feature_category: :importers do | ||||||
|   let_it_be(:project) { create(:project, :repository) } |   let_it_be(:project) { create(:project, :repository) } | ||||||
|   let(:client) { double } |   let(:client) { double } | ||||||
|   let(:source_sha) { create(:commit, project: project).id } |   let(:source_sha) { create(:commit, project: project).id } | ||||||
|  | @ -136,12 +136,6 @@ RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do | ||||||
|         expect(pull_request.attributes.fetch(:assignee_id)).to be_nil |         expect(pull_request.attributes.fetch(:assignee_id)).to be_nil | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns GitLab user id associated with GitHub id as assignee_id' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id associated with GitHub email as assignee_id' do |       it 'returns GitLab user id associated with GitHub email as assignee_id' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  | @ -156,12 +150,6 @@ RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do | ||||||
|         expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id |         expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns GitLab user id associated with GitHub id as author_id' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id associated with GitHub email as author_id' do |       it 'returns GitLab user id associated with GitHub email as author_id' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  | @ -169,7 +157,7 @@ RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns description without created at tag line' do |       it 'returns description without created at tag line' do | ||||||
|         create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |         create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|         expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes') |         expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes') | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do | RSpec.describe Gitlab::LegacyGithubImport::UserFormatter, feature_category: :importers do | ||||||
|   let(:client) { double } |   let(:client) { double } | ||||||
|   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } |   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } | ||||||
|   let(:gitea_ghost) { { id: -1, login: 'Ghost', email: '' } } |   let(:gitea_ghost) { { id: -1, login: 'Ghost', email: '' } } | ||||||
|  | @ -15,12 +15,6 @@ RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when GitHub user is a GitLab user' do |     context 'when GitHub user is a GitLab user' do | ||||||
|       it 'return GitLab user id when user associated their account with GitHub' do |  | ||||||
|         gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github') |  | ||||||
| 
 |  | ||||||
|         expect(user.gitlab_id).to eq gl_user.id |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns GitLab user id when user confirmed primary email matches GitHub email' do |       it 'returns GitLab user id when user confirmed primary email matches GitHub email' do | ||||||
|         gl_user = create(:user, email: octocat[:email]) |         gl_user = create(:user, email: octocat[:email]) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,41 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| require_migration! |  | ||||||
| 
 |  | ||||||
| RSpec.describe ChangeICodeReviewCreateMrKeysFromRedisHllToRedis, :migration, :clean_gitlab_redis_cache, feature_category: :service_ping do |  | ||||||
|   def set_redis_hll(key, value) |  | ||||||
|     Gitlab::Redis::HLL.add(key: key, value: value, expiry: 6.weeks) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def get_int_from_redis(key) |  | ||||||
|     Gitlab::Redis::SharedState.with { |redis| redis.get(key)&.to_i } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe "#up" do |  | ||||||
|     before do |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-16', 1) |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-16', 2) |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-47', 3) |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-48', 1) |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-49', 2) |  | ||||||
|       set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-49', 4) |  | ||||||
|       set_redis_hll('{hll_counters}_some_other_event-2023-49', 7) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'migrates all RedisHLL keys for i_code_review_create_mr', :aggregate_failures do |  | ||||||
|       migrate! |  | ||||||
| 
 |  | ||||||
|       expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-16')).to eq(2) |  | ||||||
|       expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-47')).to eq(1) |  | ||||||
|       expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-48')).to eq(1) |  | ||||||
|       expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-49')).to eq(2) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'does not not migrate other RedisHLL keys' do |  | ||||||
|       migrate! |  | ||||||
| 
 |  | ||||||
|       expect(get_int_from_redis('{event_counters}_some_other_event-2023-16')).to be_nil |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -12,13 +12,21 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | ||||||
| 
 | 
 | ||||||
|     describe '#maven_duplicates_allowed' do |     describe '#maven_duplicates_allowed' do | ||||||
|       it { is_expected.to validate_inclusion_of(:maven_duplicates_allowed).in_array([true, false]) } |       it { is_expected.to validate_inclusion_of(:maven_duplicates_allowed).in_array([true, false]) } | ||||||
|       it { is_expected.to validate_inclusion_of(:generic_duplicates_allowed).in_array([true, false]) } |       it { is_expected.to validate_length_of(:maven_duplicate_exception_regex).is_at_most(255) } | ||||||
|       it { is_expected.to validate_inclusion_of(:nuget_duplicates_allowed).in_array([true, false]) } |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it { is_expected.to allow_value(true, false).for(:nuget_symbol_server_enabled) } |     it { is_expected.to allow_value(true, false).for(:nuget_symbol_server_enabled) } | ||||||
|     it { is_expected.not_to allow_value(nil).for(:nuget_symbol_server_enabled) } |     it { is_expected.not_to allow_value(nil).for(:nuget_symbol_server_enabled) } | ||||||
| 
 | 
 | ||||||
|  |     it { is_expected.to validate_inclusion_of(:generic_duplicates_allowed).in_array([true, false]) } | ||||||
|  |     it { is_expected.to validate_length_of(:generic_duplicate_exception_regex).is_at_most(255) } | ||||||
|  |     it { is_expected.to validate_inclusion_of(:nuget_duplicates_allowed).in_array([true, false]) } | ||||||
|  |     it { is_expected.to validate_length_of(:nuget_duplicate_exception_regex).is_at_most(255) } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to allow_value(true, false).for(:terraform_module_duplicates_allowed) } | ||||||
|  |     it { is_expected.not_to allow_value(nil).for(:terraform_module_duplicates_allowed) } | ||||||
|  |     it { is_expected.to validate_length_of(:terraform_module_duplicate_exception_regex).is_at_most(255) } | ||||||
|  | 
 | ||||||
|     describe 'regex values' do |     describe 'regex values' do | ||||||
|       let_it_be(:package_settings) { create(:namespace_package_setting) } |       let_it_be(:package_settings) { create(:namespace_package_setting) } | ||||||
| 
 | 
 | ||||||
|  | @ -39,6 +47,50 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe 'scopes' do | ||||||
|  |     describe '.namespace_id_in' do | ||||||
|  |       let_it_be(:package_settings) { create(:namespace_package_setting) } | ||||||
|  |       let_it_be(:other_package_settings) { create(:namespace_package_setting) } | ||||||
|  | 
 | ||||||
|  |       subject { described_class.namespace_id_in([package_settings.namespace_id]) } | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to eq([package_settings]) } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe '.with_terraform_module_duplicates_allowed_or_exception_regex' do | ||||||
|  |       let_it_be(:package_settings) { create(:namespace_package_setting) } | ||||||
|  | 
 | ||||||
|  |       subject { described_class.with_terraform_module_duplicates_allowed_or_exception_regex } | ||||||
|  | 
 | ||||||
|  |       context 'when terraform_module_duplicates_allowed is true' do | ||||||
|  |         before do | ||||||
|  |           package_settings.update_column(:terraform_module_duplicates_allowed, true) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to eq([package_settings]) } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when terraform_module_duplicate_exception_regex is present' do | ||||||
|  |         before do | ||||||
|  |           package_settings.update_column(:terraform_module_duplicate_exception_regex, 'foo') | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to eq([package_settings]) } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when terraform_module_duplicates_allowed is false and terraform_module_duplicate_exception_regex is empty' do | ||||||
|  |         before do | ||||||
|  |           package_settings.update_columns( | ||||||
|  |             terraform_module_duplicates_allowed: false, | ||||||
|  |             terraform_module_duplicate_exception_regex: '' | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to be_empty } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#duplicates_allowed?' do |   describe '#duplicates_allowed?' do | ||||||
|     using RSpec::Parameterized::TableSyntax |     using RSpec::Parameterized::TableSyntax | ||||||
| 
 | 
 | ||||||
|  | @ -46,9 +98,14 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | ||||||
| 
 | 
 | ||||||
|     context 'package types with package_settings' do |     context 'package types with package_settings' do | ||||||
|       # As more package types gain settings they will be added to this list |       # As more package types gain settings they will be added to this list | ||||||
|       %i[maven_package generic_package nuget_package].each do |format| |       [ | ||||||
|         context "with package_type:#{format}" do |         { format: :maven_package, package_name: 'foo' }, | ||||||
|           let_it_be(:package) { create(format, name: 'foo', version: '1.0.0-beta') } |         { format: :generic_package, package_name: 'foo' }, | ||||||
|  |         { format: :nuget_package, package_name: 'foo' }, | ||||||
|  |         { format: :terraform_module_package, package_name: 'foo/bar' } | ||||||
|  |       ].each do |type| | ||||||
|  |         context "with package_type: #{type[:format]}" do | ||||||
|  |           let_it_be(:package) { create(type[:format], name: type[:package_name], version: '1.0.0-beta') } | ||||||
|           let_it_be(:package_type) { package.package_type } |           let_it_be(:package_type) { package.package_type } | ||||||
|           let_it_be(:package_setting) { package.project.namespace.package_settings } |           let_it_be(:package_setting) { package.project.namespace.package_settings } | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +118,7 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           with_them do |           with_them do | ||||||
|             context "for #{format}" do |             context "for #{type[:format]}" do | ||||||
|               before do |               before do | ||||||
|                 package_setting.update!( |                 package_setting.update!( | ||||||
|                   "#{package_type}_duplicates_allowed" => duplicates_allowed, |                   "#{package_type}_duplicates_allowed" => duplicates_allowed, | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do | ||||||
| 
 | 
 | ||||||
|     context 'when admin mode is enabled', :enable_admin_mode do |     context 'when admin mode is enabled', :enable_admin_mode do | ||||||
|       it { is_expected.to be_allowed(:admin_organization) } |       it { is_expected.to be_allowed(:admin_organization) } | ||||||
|  |       it { is_expected.to be_allowed(:create_group) } | ||||||
|       it { is_expected.to be_allowed(:read_organization) } |       it { is_expected.to be_allowed(:read_organization) } | ||||||
|       it { is_expected.to be_allowed(:read_organization_user) } |       it { is_expected.to be_allowed(:read_organization_user) } | ||||||
|     end |     end | ||||||
|  | @ -36,12 +37,14 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it { is_expected.to be_allowed(:admin_organization) } |     it { is_expected.to be_allowed(:admin_organization) } | ||||||
|  |     it { is_expected.to be_allowed(:create_group) } | ||||||
|     it { is_expected.to be_allowed(:read_organization) } |     it { is_expected.to be_allowed(:read_organization) } | ||||||
|     it { is_expected.to be_allowed(:read_organization_user) } |     it { is_expected.to be_allowed(:read_organization_user) } | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   context 'when the user is not part of the organization' do |   context 'when the user is not part of the organization' do | ||||||
|     it { is_expected.to be_disallowed(:admin_organization) } |     it { is_expected.to be_disallowed(:admin_organization) } | ||||||
|  |     it { is_expected.to be_disallowed(:create_group) } | ||||||
|     it { is_expected.to be_disallowed(:read_organization_user) } |     it { is_expected.to be_disallowed(:read_organization_user) } | ||||||
|     # All organizations are currently public, and hence they are allowed to be read |     # All organizations are currently public, and hence they are allowed to be read | ||||||
|     # even if the user is not a part of the organization. |     # even if the user is not a part of the organization. | ||||||
|  |  | ||||||
|  | @ -23,7 +23,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | ||||||
|       lock_npm_package_requests_forwarding: true, |       lock_npm_package_requests_forwarding: true, | ||||||
|       pypi_package_requests_forwarding: true, |       pypi_package_requests_forwarding: true, | ||||||
|       lock_pypi_package_requests_forwarding: true, |       lock_pypi_package_requests_forwarding: true, | ||||||
|       nuget_symbol_server_enabled: true |       nuget_symbol_server_enabled: true, | ||||||
|  |       terraform_module_duplicates_allowed: true, | ||||||
|  |       terraform_module_duplicate_exception_regex: 'foo-.*' | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -44,6 +46,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | ||||||
|           pypiPackageRequestsForwarding |           pypiPackageRequestsForwarding | ||||||
|           lockPypiPackageRequestsForwarding |           lockPypiPackageRequestsForwarding | ||||||
|           nugetSymbolServerEnabled |           nugetSymbolServerEnabled | ||||||
|  |           terraformModuleDuplicatesAllowed | ||||||
|  |           terraformModuleDuplicateExceptionRegex | ||||||
|         } |         } | ||||||
|         errors |         errors | ||||||
|       QL |       QL | ||||||
|  | @ -73,6 +77,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | ||||||
|       expect(package_settings_response['npmPackageRequestsForwarding']).to eq(params[:npm_package_requests_forwarding]) |       expect(package_settings_response['npmPackageRequestsForwarding']).to eq(params[:npm_package_requests_forwarding]) | ||||||
|       expect(package_settings_response['lockNpmPackageRequestsForwarding']).to eq(params[:lock_npm_package_requests_forwarding]) |       expect(package_settings_response['lockNpmPackageRequestsForwarding']).to eq(params[:lock_npm_package_requests_forwarding]) | ||||||
|       expect(package_settings_response['nugetSymbolServerEnabled']).to eq(params[:nuget_symbol_server_enabled]) |       expect(package_settings_response['nugetSymbolServerEnabled']).to eq(params[:nuget_symbol_server_enabled]) | ||||||
|  |       expect(package_settings_response['terraformModuleDuplicatesAllowed']).to eq(params[:terraform_module_duplicates_allowed]) | ||||||
|  |       expect(package_settings_response['terraformModuleDuplicateExceptionRegex']).to eq(params[:terraform_module_duplicate_exception_regex]) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -115,7 +121,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | ||||||
|         lock_npm_package_requests_forwarding: false, |         lock_npm_package_requests_forwarding: false, | ||||||
|         pypi_package_requests_forwarding: nil, |         pypi_package_requests_forwarding: nil, | ||||||
|         lock_pypi_package_requests_forwarding: false, |         lock_pypi_package_requests_forwarding: false, | ||||||
|         nuget_symbol_server_enabled: false |         nuget_symbol_server_enabled: false, | ||||||
|  |         terraform_module_duplicates_allowed: false, | ||||||
|  |         terraform_module_duplicate_exception_regex: 'foo' | ||||||
|       }, to: { |       }, to: { | ||||||
|         maven_duplicates_allowed: false, |         maven_duplicates_allowed: false, | ||||||
|         maven_duplicate_exception_regex: 'foo-.*', |         maven_duplicate_exception_regex: 'foo-.*', | ||||||
|  | @ -129,7 +137,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | ||||||
|         lock_npm_package_requests_forwarding: true, |         lock_npm_package_requests_forwarding: true, | ||||||
|         pypi_package_requests_forwarding: true, |         pypi_package_requests_forwarding: true, | ||||||
|         lock_pypi_package_requests_forwarding: true, |         lock_pypi_package_requests_forwarding: true, | ||||||
|         nuget_symbol_server_enabled: true |         nuget_symbol_server_enabled: true, | ||||||
|  |         terraform_module_duplicates_allowed: true, | ||||||
|  |         terraform_module_duplicate_exception_regex: 'foo-.*' | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'returning a success' |     it_behaves_like 'returning a success' | ||||||
|  |  | ||||||
|  | @ -1937,6 +1937,59 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when group is within a provided organization' do | ||||||
|  |       let_it_be(:organization) { create(:organization) } | ||||||
|  | 
 | ||||||
|  |       context 'when user is an organization user' do | ||||||
|  |         before_all do | ||||||
|  |           create(:organization_user, user: user3, organization: organization) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'creates group within organization' do | ||||||
|  |           post api('/groups', user3), params: attributes_for_group_api(organization_id: organization.id) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:created) | ||||||
|  |           expect(json_response['organization_id']).to eq(organization.id) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when parent_group is not part of the organization' do | ||||||
|  |           it 'does not create the group with not_found' do | ||||||
|  |             post( | ||||||
|  |               api('/groups', user3), | ||||||
|  |               params: attributes_for_group_api(parent_id: group2.id, organization_id: organization.id) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             expect(response).to have_gitlab_http_status(:not_found) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when organization does not exist' do | ||||||
|  |         it 'does not create the group with not_found' do | ||||||
|  |           post api('/groups', user3), params: attributes_for_group_api(organization_id: non_existing_record_id) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:not_found) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when user is not an organization user' do | ||||||
|  |         it 'does not create the group' do | ||||||
|  |           post api('/groups', user3), params: attributes_for_group_api(organization_id: organization.id) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:forbidden) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when user is an admin' do | ||||||
|  |         it 'creates group within organization' do | ||||||
|  |           post api('/groups', admin, admin_mode: true), params: attributes_for_group_api(organization_id: organization.id) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:created) | ||||||
|  |           expect(json_response['organization_id']).to eq(organization.id) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context "when authenticated as user with group permissions" do |     context "when authenticated as user with group permissions" do | ||||||
|       it "creates group", :aggregate_failures do |       it "creates group", :aggregate_failures do | ||||||
|         group = attributes_for_group_api request_access_enabled: false |         group = attributes_for_group_api request_access_enabled: false | ||||||
|  |  | ||||||
|  | @ -103,7 +103,28 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     shared_examples 'creating a package' do | ||||||
|  |       it 'creates a package' do | ||||||
|  |         expect { api_request } | ||||||
|  |           .to change { project.packages.count }.by(1) | ||||||
|  |           .and change { Packages::PackageFile.count }.by(1) | ||||||
|  |         expect(response).to have_gitlab_http_status(:created) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     shared_examples 'not creating a package' do |expected_status| | ||||||
|  |       it 'does not create a package' do | ||||||
|  |         expect { api_request } | ||||||
|  |           .to change { project.packages.count }.by(0) | ||||||
|  |           .and change { Packages::PackageFile.count }.by(0) | ||||||
|  |         expect(response).to have_gitlab_http_status(expected_status) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'with valid project' do |     context 'with valid project' do | ||||||
|  |       let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } } | ||||||
|  |       let(:headers) { user_headers.merge(workhorse_headers) } | ||||||
|  | 
 | ||||||
|       where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do |       where(:visibility, :user_role, :member, :token_header, :token_type, :shared_examples_name, :expected_status) do | ||||||
|         :public  | :developer  | true  | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload'          | :created |         :public  | :developer  | true  | 'PRIVATE-TOKEN' | :personal_access_token | 'process terraform module upload'          | :created | ||||||
|         :public  | :guest      | true  | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden |         :public  | :guest      | true  | 'PRIVATE-TOKEN' | :personal_access_token | 'rejects terraform module packages access' | :forbidden | ||||||
|  | @ -147,7 +168,6 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | ||||||
| 
 | 
 | ||||||
|       with_them do |       with_them do | ||||||
|         let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } |         let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } | ||||||
|         let(:headers) { user_headers.merge(workhorse_headers) } |  | ||||||
|         let(:snowplow_gitlab_standard_context) do |         let(:snowplow_gitlab_standard_context) do | ||||||
|           { project: project, namespace: project.namespace, user: snowplow_user, |           { project: project, namespace: project.namespace, user: snowplow_user, | ||||||
|             property: 'i_package_terraform_module_user' } |             property: 'i_package_terraform_module_user' } | ||||||
|  | @ -172,43 +192,73 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when failed package file save' do |       context 'when failed package file save' do | ||||||
|         let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } } |         before do | ||||||
|         let(:headers) { user_headers.merge(workhorse_headers) } |           project.add_developer(user) | ||||||
|  |           allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'not creating a package', :error | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with an existing package in the same project' do | ||||||
|  |         let_it_be_with_reload(:existing_package) do | ||||||
|  |           create(:terraform_module_package, name: 'mymodule/mysystem', version: '1.0.0', project: project) | ||||||
|  |         end | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           project.add_developer(user) |           project.add_developer(user) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'does not create package record', :aggregate_failures do |         it_behaves_like 'not creating a package', :forbidden | ||||||
|           allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError) |  | ||||||
| 
 | 
 | ||||||
|           expect { api_request } |         context 'when marked as pending_destruction' do | ||||||
|               .to change { project.packages.count }.by(0) |           before do | ||||||
|               .and change { Packages::PackageFile.count }.by(0) |             existing_package.pending_destruction! | ||||||
|           expect(response).to have_gitlab_http_status(:error) |           end | ||||||
|  | 
 | ||||||
|  |           it_behaves_like 'creating a package' | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with existing package in another project' do | ||||||
|  |         let_it_be(:package_settings) { create(:namespace_package_setting, namespace: group) } | ||||||
|  |         let_it_be(:project2) { create(:project, namespace: group) } | ||||||
|  |         let!(:existing_package) { create(:terraform_module_package, name: 'mymodule/mysystem', project: project2) } | ||||||
|  | 
 | ||||||
|  |         before do | ||||||
|  |           project.add_developer(user) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'with an existing package' do |         context 'when duplicates not allowed' do | ||||||
|           let_it_be_with_reload(:existing_package) do |           it_behaves_like 'not creating a package', :forbidden | ||||||
|             create(:terraform_module_package, name: 'mymodule/mysystem', version: '1.0.0', project: project) |         end | ||||||
|  | 
 | ||||||
|  |         context 'when duplicates allowed' do | ||||||
|  |           before do | ||||||
|  |             package_settings.update_column(:terraform_module_duplicates_allowed, true) | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           it 'does not create a new package' do |           it_behaves_like 'creating a package' | ||||||
|             expect { api_request } |         end | ||||||
|               .to change { project.packages.count }.by(0) | 
 | ||||||
|               .and change { Packages::PackageFile.count }.by(0) |         context 'with duplicate regex exception' do | ||||||
|             expect(response).to have_gitlab_http_status(:forbidden) |           before do | ||||||
|  |             package_settings.update_columns( | ||||||
|  |               terraform_module_duplicates_allowed: false, | ||||||
|  |               terraform_module_duplicate_exception_regex: regex | ||||||
|  |             ) | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           context 'when marked as pending_destruction' do |           context 'when regex matches' do | ||||||
|             it 'does create a new package' do |             let(:regex) { ".*#{existing_package.name.last(3)}.*" } | ||||||
|               existing_package.pending_destruction! |  | ||||||
| 
 | 
 | ||||||
|               expect { api_request } |             it_behaves_like 'creating a package' | ||||||
|                 .to change { project.packages.count }.by(1) |           end | ||||||
|                 .and change { Packages::PackageFile.count }.by(1) | 
 | ||||||
|               expect(response).to have_gitlab_http_status(:created) |           context 'when regex does not match' do | ||||||
|             end |             let(:regex) { '.*non-matching-regex.*' } | ||||||
|  | 
 | ||||||
|  |             it_behaves_like 'not creating a package', :forbidden | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ | ||||||
|   let!(:user) { create(:user) } |   let!(:user) { create(:user) } | ||||||
|   let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } |   let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } | ||||||
| 
 | 
 | ||||||
|   subject { service.execute } |   subject(:execute) { service.execute } | ||||||
| 
 | 
 | ||||||
|   shared_examples 'has sync-ed traversal_ids' do |   shared_examples 'has sync-ed traversal_ids' do | ||||||
|     specify { expect(subject.reload.traversal_ids).to eq([subject.parent&.traversal_ids, subject.id].flatten.compact) } |     specify { expect(subject.reload.traversal_ids).to eq([subject.parent&.traversal_ids, subject.id].flatten.compact) } | ||||||
|  | @ -119,6 +119,49 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe 'creating a group within an organization' do | ||||||
|  |     let(:current_user) { user } | ||||||
|  |     let(:service) { described_class.new(current_user, params) } | ||||||
|  | 
 | ||||||
|  |     context 'when organization is provided' do | ||||||
|  |       let_it_be(:organization) { create(:organization) } | ||||||
|  |       let(:params) { group_params.merge(organization_id: organization.id) } | ||||||
|  | 
 | ||||||
|  |       context 'when user can create the group' do | ||||||
|  |         before do | ||||||
|  |           create(:organization_user, user: user, organization: organization) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to be_persisted } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when user is an admin', :enable_admin_mode do | ||||||
|  |         let(:current_user) { create(:admin) } | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to be_persisted } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when user can not create the group' do | ||||||
|  |         it 'does not save group and returns an error' do | ||||||
|  |           expect(execute).not_to be_persisted | ||||||
|  |           expect(execute.errors[:organization_id].first) | ||||||
|  |             .to eq(s_("CreateGroup|You don't have permission to create a group in the provided organization.")) | ||||||
|  |           expect(execute.organization_id).to be_nil | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when organization is the default organization and not set by params' do | ||||||
|  |       let(:params) { group_params } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         create(:organization, :default) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to be_persisted } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'creating subgroup' do |   describe 'creating subgroup' do | ||||||
|     let!(:group) { create(:group) } |     let!(:group) { create(:group) } | ||||||
|     let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } |     let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } | ||||||
|  |  | ||||||
|  | @ -46,7 +46,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: : | ||||||
|           lock_npm_package_requests_forwarding: false, |           lock_npm_package_requests_forwarding: false, | ||||||
|           pypi_package_requests_forwarding: nil, |           pypi_package_requests_forwarding: nil, | ||||||
|           lock_pypi_package_requests_forwarding: false, |           lock_pypi_package_requests_forwarding: false, | ||||||
|           nuget_symbol_server_enabled: false |           nuget_symbol_server_enabled: false, | ||||||
|  |           terraform_module_duplicates_allowed: false, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'foo' | ||||||
|         }, to: { |         }, to: { | ||||||
|           maven_duplicates_allowed: false, |           maven_duplicates_allowed: false, | ||||||
|           maven_duplicate_exception_regex: 'RELEASE', |           maven_duplicate_exception_regex: 'RELEASE', | ||||||
|  | @ -60,7 +62,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: : | ||||||
|           lock_npm_package_requests_forwarding: true, |           lock_npm_package_requests_forwarding: true, | ||||||
|           pypi_package_requests_forwarding: true, |           pypi_package_requests_forwarding: true, | ||||||
|           lock_pypi_package_requests_forwarding: true, |           lock_pypi_package_requests_forwarding: true, | ||||||
|           nuget_symbol_server_enabled: true |           nuget_symbol_server_enabled: true, | ||||||
|  |           terraform_module_duplicates_allowed: true, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'bar' | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|       it_behaves_like 'returning a success' |       it_behaves_like 'returning a success' | ||||||
|  | @ -112,7 +116,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: : | ||||||
|           lock_npm_package_requests_forwarding: true, |           lock_npm_package_requests_forwarding: true, | ||||||
|           pypi_package_requests_forwarding: true, |           pypi_package_requests_forwarding: true, | ||||||
|           lock_pypi_package_requests_forwarding: true, |           lock_pypi_package_requests_forwarding: true, | ||||||
|           nuget_symbol_server_enabled: true |           nuget_symbol_server_enabled: true, | ||||||
|  |           terraform_module_duplicates_allowed: true, | ||||||
|  |           terraform_module_duplicate_exception_regex: 'bar' | ||||||
|         } |         } | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,10 +2,11 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category: :package_registry do | RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category: :package_registry do | ||||||
|   let_it_be(:namespace) { create(:namespace) } |   let_it_be(:namespace) { create(:group) } | ||||||
|   let_it_be(:project) { create(:project, namespace: namespace) } |   let_it_be(:project) { create(:project, namespace: namespace) } | ||||||
|   let_it_be(:user) { create(:user) } |   let_it_be(:user) { create(:user) } | ||||||
|   let_it_be(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } |   let_it_be(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } | ||||||
|  |   let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) } | ||||||
| 
 | 
 | ||||||
|   let(:overrides) { {} } |   let(:overrides) { {} } | ||||||
| 
 | 
 | ||||||
|  | @ -36,10 +37,72 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | ||||||
| 
 | 
 | ||||||
|     context 'package already exists elsewhere' do |     context 'package already exists elsewhere' do | ||||||
|       let(:project2) { create(:project, namespace: namespace) } |       let(:project2) { create(:project, namespace: namespace) } | ||||||
|       let!(:existing_package) { create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') } |       let!(:existing_package) do | ||||||
|  |         create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') | ||||||
|  |       end | ||||||
| 
 | 
 | ||||||
|       it { expect(subject[:http_status]).to eq 403 } |       context 'when duplicates not allowed' do | ||||||
|       it { expect(subject[:message]).to be 'Access Denied' } |         it { expect(subject.reason).to eq :forbidden } | ||||||
|  |         it { expect(subject.message).to be 'A package with the same name already exists in the namespace' } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when duplicates allowed' do | ||||||
|  |         before do | ||||||
|  |           package_settings.update_column(:terraform_module_duplicates_allowed, true) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'creating a package' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with duplicate regex exception' do | ||||||
|  |         before do | ||||||
|  |           package_settings.update_columns( | ||||||
|  |             terraform_module_duplicates_allowed: false, | ||||||
|  |             terraform_module_duplicate_exception_regex: regex | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when regex matches' do | ||||||
|  |           let(:regex) { ".*#{existing_package.name.last(3)}.*" } | ||||||
|  | 
 | ||||||
|  |           it_behaves_like 'creating a package' | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when regex does not match' do | ||||||
|  |           let(:regex) { '.*not-a-match.*' } | ||||||
|  | 
 | ||||||
|  |           it { expect(subject.reason).to eq :forbidden } | ||||||
|  |           it { expect(subject.message).to be 'A package with the same name already exists in the namespace' } | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'for ancestor namespace' do | ||||||
|  |         let_it_be(:package_settings) { create(:namespace_package_setting, :group) } | ||||||
|  |         let_it_be(:parent_namespace) { package_settings.namespace } | ||||||
|  | 
 | ||||||
|  |         before do | ||||||
|  |           namespace.update!(parent: parent_namespace) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when duplicates allowed in an ancestor' do | ||||||
|  |           before do | ||||||
|  |             package_settings.update_column(:terraform_module_duplicates_allowed, true) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it_behaves_like 'creating a package' | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when duplicates allowed in an ancestor with exception' do | ||||||
|  |           before do | ||||||
|  |             package_settings.update_columns( | ||||||
|  |               terraform_module_duplicates_allowed: false, | ||||||
|  |               terraform_module_duplicate_exception_regex: ".*#{existing_package.name.last(3)}.*" | ||||||
|  |             ) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it_behaves_like 'creating a package' | ||||||
|  |         end | ||||||
|  |       end | ||||||
| 
 | 
 | ||||||
|       context 'marked as pending_destruction' do |       context 'marked as pending_destruction' do | ||||||
|         before do |         before do | ||||||
|  | @ -53,7 +116,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | ||||||
|     context 'version already exists' do |     context 'version already exists' do | ||||||
|       let!(:existing_version) { create(:terraform_module_package, project: project, name: 'foo/bar', version: '1.0.1') } |       let!(:existing_version) { create(:terraform_module_package, project: project, name: 'foo/bar', version: '1.0.1') } | ||||||
| 
 | 
 | ||||||
|       it { expect(subject[:http_status]).to eq 403 } |       it { expect(subject[:reason]).to eq :forbidden } | ||||||
|       it { expect(subject[:message]).to be 'Package version already exists.' } |       it { expect(subject[:message]).to be 'Package version already exists.' } | ||||||
| 
 | 
 | ||||||
|       context 'marked as pending_destruction' do |       context 'marked as pending_destruction' do | ||||||
|  | @ -68,7 +131,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | ||||||
|     context 'with empty version' do |     context 'with empty version' do | ||||||
|       let(:overrides) { { module_version: '' } } |       let(:overrides) { { module_version: '' } } | ||||||
| 
 | 
 | ||||||
|       it { expect(subject[:http_status]).to eq 400 } |       it { expect(subject[:reason]).to eq :bad_request } | ||||||
|       it { expect(subject[:message]).to eq 'Version is empty.' } |       it { expect(subject[:message]).to eq 'Version is empty.' } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -12,6 +12,8 @@ RSpec.shared_examples 'updating the namespace package setting attributes' do |to | ||||||
|       .and change { namespace.package_settings.reload.nuget_duplicates_allowed }.from(from[:nuget_duplicates_allowed]).to(to[:nuget_duplicates_allowed]) |       .and change { namespace.package_settings.reload.nuget_duplicates_allowed }.from(from[:nuget_duplicates_allowed]).to(to[:nuget_duplicates_allowed]) | ||||||
|       .and change { namespace.package_settings.reload.nuget_duplicate_exception_regex }.from(from[:nuget_duplicate_exception_regex]).to(to[:nuget_duplicate_exception_regex]) |       .and change { namespace.package_settings.reload.nuget_duplicate_exception_regex }.from(from[:nuget_duplicate_exception_regex]).to(to[:nuget_duplicate_exception_regex]) | ||||||
|       .and change { namespace.package_settings.reload.nuget_symbol_server_enabled }.from(from[:nuget_symbol_server_enabled]).to(to[:nuget_symbol_server_enabled]) |       .and change { namespace.package_settings.reload.nuget_symbol_server_enabled }.from(from[:nuget_symbol_server_enabled]).to(to[:nuget_symbol_server_enabled]) | ||||||
|  |       .and change { namespace.package_settings.reload.terraform_module_duplicates_allowed }.from(from[:terraform_module_duplicates_allowed]).to(to[:terraform_module_duplicates_allowed]) | ||||||
|  |       .and change { namespace.package_settings.reload.terraform_module_duplicate_exception_regex }.from(from[:terraform_module_duplicate_exception_regex]).to(to[:terraform_module_duplicate_exception_regex]) | ||||||
|   end |   end | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
|  | @ -36,6 +38,8 @@ RSpec.shared_examples 'creating the namespace package setting' do | ||||||
|     expect(namespace.package_setting_relation.nuget_duplicates_allowed).to eq(package_settings[:nuget_duplicates_allowed]) |     expect(namespace.package_setting_relation.nuget_duplicates_allowed).to eq(package_settings[:nuget_duplicates_allowed]) | ||||||
|     expect(namespace.package_setting_relation.nuget_duplicate_exception_regex).to eq(package_settings[:nuget_duplicate_exception_regex]) |     expect(namespace.package_setting_relation.nuget_duplicate_exception_regex).to eq(package_settings[:nuget_duplicate_exception_regex]) | ||||||
|     expect(namespace.package_setting_relation.nuget_symbol_server_enabled).to eq(package_settings[:nuget_symbol_server_enabled]) |     expect(namespace.package_setting_relation.nuget_symbol_server_enabled).to eq(package_settings[:nuget_symbol_server_enabled]) | ||||||
|  |     expect(namespace.package_setting_relation.terraform_module_duplicates_allowed).to eq(package_settings[:terraform_module_duplicates_allowed]) | ||||||
|  |     expect(namespace.package_setting_relation.terraform_module_duplicate_exception_regex).to eq(package_settings[:terraform_module_duplicate_exception_regex]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   it_behaves_like 'returning a success' |   it_behaves_like 'returning a success' | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue