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/update_package_from_metadata_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/post_receive_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/legacy_github_import/branch_formatter_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/issue_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/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/user_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/client_spec.rb' | ||||
|  |  | |||
|  | @ -46,6 +46,7 @@ export default () => { | |||
|         gl.mrWidgetData.can_create_pipeline_in_target_project, | ||||
|       ), | ||||
|       commitPathTemplate: gl.mrWidgetData.commit_path_template, | ||||
|       canAdminVulnerability: gl.mrWidgetData.can_admin_vulnerability, | ||||
|       dismissalDescriptions, | ||||
|     }, | ||||
|     ...MrWidgetOptions, | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ export default { | |||
|       :key="opt.value" | ||||
|       :disabled="!!opt.disabled" | ||||
|       :selected="value === opt.value" | ||||
|       v-bind="opt.props" | ||||
|       @click="$emit('input', opt.value)" | ||||
|     > | ||||
|       <slot name="button-content" v-bind="opt">{{ opt.text }}</slot> | ||||
|  |  | |||
|  | @ -51,6 +51,16 @@ module Mutations | |||
|                 required: false, | ||||
|                 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, | ||||
|                 GraphQL::Types::Boolean, | ||||
|                 required: false, | ||||
|  |  | |||
|  | @ -35,6 +35,12 @@ module Types | |||
|     field :pypi_package_requests_forwarding, GraphQL::Types::Boolean, | ||||
|       null: true, | ||||
|       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, | ||||
|       null: false, | ||||
|  |  | |||
|  | @ -1,14 +1,6 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| 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) | ||||
|     [deployment.project, deployment.deployable] | ||||
|   end | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ class Namespace::PackageSetting < ApplicationRecord | |||
| 
 | ||||
|   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 | ||||
| 
 | ||||
|  | @ -24,6 +24,14 @@ class Namespace::PackageSetting < ApplicationRecord | |||
|   validates :nuget_duplicates_allowed, inclusion: { in: [true, false] } | ||||
|   validates :nuget_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } | ||||
|   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 | ||||
|     def duplicates_allowed?(package) | ||||
|  |  | |||
|  | @ -13,12 +13,14 @@ module Organizations | |||
| 
 | ||||
|     rule { admin }.policy do | ||||
|       enable :admin_organization | ||||
|       enable :create_group | ||||
|       enable :read_organization | ||||
|       enable :read_organization_user | ||||
|     end | ||||
| 
 | ||||
|     rule { organization_user }.policy do | ||||
|       enable :admin_organization | ||||
|       enable :create_group | ||||
|       enable :read_organization | ||||
|       enable :read_organization_user | ||||
|     end | ||||
|  |  | |||
|  | @ -92,9 +92,32 @@ module Groups | |||
|         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 | ||||
|     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? | ||||
|       unless Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level) | ||||
|         deny_visibility_level(@group) | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ module Namespaces | |||
|                               maven_package_requests_forwarding | ||||
|                               nuget_duplicates_allowed | ||||
|                               nuget_duplicate_exception_regex | ||||
|                               terraform_module_duplicates_allowed | ||||
|                               terraform_module_duplicate_exception_regex | ||||
|                               npm_package_requests_forwarding | ||||
|                               pypi_package_requests_forwarding | ||||
|                               lock_maven_package_requests_forwarding | ||||
|  |  | |||
|  | @ -6,10 +6,20 @@ module Packages | |||
|       include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|       def execute | ||||
|         return error('Version is empty.', 400) if params[:module_version].blank? | ||||
|         return error('Access Denied', 403) if current_package_exists_elsewhere? | ||||
|         return error('Package version already exists.', 403) if current_package_version_exists? | ||||
|         return error('File is too large.', 400) if file_size_exceeded? | ||||
|         if params[:module_version].blank? | ||||
|           return ServiceResponse.error(message: 'Version is empty.', reason: :bad_request) | ||||
|         end | ||||
| 
 | ||||
|         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! } | ||||
|       end | ||||
|  | @ -24,6 +34,15 @@ module Packages | |||
|         package | ||||
|       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? | ||||
|         ::Packages::Package | ||||
|           .for_projects(project.root_namespace.all_projects.id_not_in(project.id)) | ||||
|  | @ -62,9 +81,13 @@ module Packages | |||
|         } | ||||
|       end | ||||
| 
 | ||||
|       def file_size_exceeded? | ||||
|         project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size) | ||||
|       end | ||||
|       def package_settings_with_duplicates_allowed | ||||
|         ::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 | ||||
|       strong_memoize_attr :package_settings_with_duplicates_allowed | ||||
|     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] | ||||
|   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 | ||||
|     # For each old (redis_hll) counter we find the corresponding target (redis) counter and add | ||||
|     # 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. | ||||
|     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 | ||||
|     # no-op | ||||
|     # | ||||
|     # Removed due to https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17321 | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| a18e718e99c23ae6db929929a905af0db72e3a3734d3c33e12ec2cdb44467f6d | ||||
|  | @ -19505,9 +19505,12 @@ CREATE TABLE namespace_package_settings ( | |||
|     nuget_duplicates_allowed boolean DEFAULT true NOT NULL, | ||||
|     nuget_duplicate_exception_regex text DEFAULT ''::text 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_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 ( | ||||
|  |  | |||
|  | @ -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="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="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 | ||||
| 
 | ||||
|  | @ -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="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="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` | ||||
| 
 | ||||
|  |  | |||
|  | @ -817,7 +817,7 @@ POST /groups | |||
| Parameters: | ||||
| 
 | ||||
| | Attribute                                               | Type    | Required | Description                                                                                                                                                                                     | | ||||
| | ------------------------------------------------------- | ------- | -------- | ----------- | | ||||
| | ------------------------------------------------------- | ------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||
| | `name`                                                  | string  | yes      | The name 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.                                                                                                                             | | ||||
|  | @ -829,6 +829,7 @@ Parameters: | |||
| | `emails_enabled`                                        | boolean | no       | Enable email notifications.                                                                                                                                                                     | | ||||
| | `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.                                                                                                                                       | | ||||
| | `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). | | ||||
| | `request_access_enabled`                                | boolean | no       | Allow users to request member access.                                                                                                                                                           | | ||||
|  |  | |||
|  | @ -70,6 +70,8 @@ Setting | Table | Description | |||
| `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_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 - `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: | ||||
| 
 | ||||
| - 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"`. | ||||
| - 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). | ||||
| 
 | ||||
| ```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. | ||||
| 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 | ||||
| 
 | ||||
| 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 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 | ||||
| 
 | ||||
| To download a Terraform module: | ||||
|  | @ -275,4 +293,4 @@ For examples of the Terraform Module Registry, check the projects below: | |||
| 
 | ||||
| ## 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 in `git` are case-sensitive. When configuring your protected branch | ||||
| or [target branch rule](repository/branches/index.md#configure-rules-for-target-branches), | ||||
| Branch names in `git` are case-sensitive. When configuring your protected branch, | ||||
| or your [target branch workflow](repository/branches/index.md#configure-workflows-for-target-branches), | ||||
| `dev` is not the same `DEV` or `Dev`. | ||||
|  |  | |||
|  | @ -287,58 +287,58 @@ To do this: | |||
| 1. 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. | ||||
| > - [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`. | ||||
| 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. | ||||
| 
 | ||||
| When you create a merge request, the rule checks the name of the branch. If the | ||||
| branch name matches the rule, the merge request targets the branch you specify | ||||
| in the rule. If the branch name does not match, the merge request targets the | ||||
| When you create a merge request, the workflow checks the name of the branch. If the | ||||
| 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 | ||||
| default branch of the project. | ||||
| 
 | ||||
| Prerequisites: | ||||
| 
 | ||||
| - 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. Select **Settings > Merge requests**. | ||||
| 1. Select **Add target branch rule**. | ||||
| 1. For **Rule name**, provide a string or wild card to compare against branch names. | ||||
| 1. Select the **Target branch** to use when the branch name matches the **Rule name**. | ||||
| 1. Scroll down to **Merge request branch workflow** | ||||
| 1. Select **Add branch target**. | ||||
| 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**. | ||||
| 
 | ||||
| ### 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`     | | ||||
| | `bug/*`     | `develop`     | | ||||
| | `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. | ||||
| - 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. | ||||
| 
 | ||||
| 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`. | ||||
| 
 | ||||
| ## 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: | ||||
| 
 | ||||
|  | @ -348,7 +348,7 @@ To do this: | |||
| 
 | ||||
| 1. On the left sidebar, select **Search or go to** and find your project. | ||||
| 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ module API | |||
|       expose :full_name, :full_path | ||||
|       expose :created_at | ||||
|       expose :parent_id | ||||
|       expose :organization_id | ||||
|       expose :shared_runners_setting | ||||
| 
 | ||||
|       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 :path, type: String, desc: 'The path of the 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 | ||||
|       end | ||||
|       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 | ||||
|           authorize! :create_subgroup, parent_group | ||||
|         else | ||||
|  |  | |||
|  | @ -211,18 +211,25 @@ module API | |||
|       not_found!('Pipeline') | ||||
|     end | ||||
| 
 | ||||
|     def find_organization!(id) | ||||
|       organization = Organizations::Organization.find_by_id(id) | ||||
|       check_organization_access(organization) | ||||
|     end | ||||
| 
 | ||||
|     # 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 | ||||
|         Group.find_by(id: id) | ||||
|         collection.find_by(id: id) | ||||
|       else | ||||
|         Group.find_by_full_path(id) | ||||
|         collection.find_by_full_path(id) | ||||
|       end | ||||
|     end | ||||
|     # rubocop: enable CodeReuse/ActiveRecord | ||||
| 
 | ||||
|     def find_group!(id) | ||||
|       group = find_group(id) | ||||
|     def find_group!(id, organization: nil) | ||||
|       group = find_group(id, organization: organization) | ||||
|       check_group_access(group) | ||||
|     end | ||||
| 
 | ||||
|  | @ -835,6 +842,12 @@ module API | |||
|       @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] | ||||
|     end | ||||
| 
 | ||||
|     def check_organization_access(organization) | ||||
|       return organization if can?(current_user, :read_organization, organization) | ||||
| 
 | ||||
|       not_found!('Organization') | ||||
|     end | ||||
| 
 | ||||
|     def secret_token | ||||
|       Gitlab::Shell.secret_token | ||||
|     end | ||||
|  |  | |||
|  | @ -171,7 +171,7 @@ module API | |||
|                       .new(authorized_user_project, current_user, create_package_file_params) | ||||
|                       .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, | ||||
|                       namespace: authorized_user_project.namespace) | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ module Gitlab | |||
|       def gitlab_id | ||||
|         return @gitlab_id if defined?(@gitlab_id) | ||||
| 
 | ||||
|         @gitlab_id = find_by_external_uid || find_by_email | ||||
|         @gitlab_id = find_by_email | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
|  | @ -45,14 +45,6 @@ module Gitlab | |||
|         User.find_by_any_email(email) | ||||
|             .try(:id) | ||||
|       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 | ||||
|  |  | |||
|  | @ -14578,6 +14578,9 @@ msgstr "" | |||
| msgid "CreateGitTag|Set tag message" | ||||
| 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." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ FactoryBot.define do | |||
| 
 | ||||
|     nuget_symbol_server_enabled { false } | ||||
| 
 | ||||
|     terraform_module_duplicates_allowed { false } | ||||
|     terraform_module_duplicate_exception_regex { 'foo' } | ||||
| 
 | ||||
|     trait :group do | ||||
|       namespace { association(:group) } | ||||
|     end | ||||
|  |  | |||
|  | @ -122,6 +122,7 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => { | |||
|       [[{ value: '1' }]], | ||||
|       [[{ value: 1, disabled: true }]], | ||||
|       [[{ value: true, disabled: false }]], | ||||
|       [[{ value: true, props: { 'data-testid': 'test' } }]], | ||||
|     ])('with options=%j, passes validation', (options) => { | ||||
|       createComponent({ options }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,7 +39,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | |||
|           lock_npm_package_requests_forwarding: false, | ||||
|           pypi_package_requests_forwarding: nil, | ||||
|           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: { | ||||
|           maven_duplicates_allowed: false, | ||||
|           maven_duplicate_exception_regex: 'RELEASE', | ||||
|  | @ -53,7 +55,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | |||
|           lock_npm_package_requests_forwarding: true, | ||||
|           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' | ||||
|  | @ -109,7 +113,9 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category: | |||
|           lock_npm_package_requests_forwarding: true, | ||||
|           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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,6 +33,8 @@ RSpec.describe GitlabSchema.types['PackageSettings'], feature_category: :package | |||
|       npm_package_requests_forwarding_locked | ||||
|       pypi_package_requests_forwarding_locked | ||||
|       nuget_symbol_server_enabled | ||||
|       terraform_module_duplicates_allowed | ||||
|       terraform_module_duplicate_exception_regex | ||||
|     ] | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|   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 | ||||
|     let_it_be(:group) { create(:group, :public) } | ||||
|     let_it_be(:user) { create(:user) } | ||||
|  | @ -457,7 +488,7 @@ RSpec.describe API::Helpers, feature_category: :shared do | |||
|       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(:user) { group.first_owner } | ||||
|  | @ -505,6 +536,34 @@ RSpec.describe API::Helpers, feature_category: :shared do | |||
|     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 | ||||
|     let_it_be(:group) { create(:group, :public) } | ||||
|     let_it_be(:user) { create(:user) } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| 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(:client) { double } | ||||
|   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 | ||||
|       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 | ||||
|         gl_user = create(:user, email: octocat[:email]) | ||||
| 
 | ||||
|  | @ -89,7 +83,7 @@ RSpec.describe Gitlab::LegacyGithubImport::CommentFormatter do | |||
|       end | ||||
| 
 | ||||
|       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.") | ||||
|       end | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| 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(:client) { double } | ||||
|   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 | ||||
|       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 | ||||
|         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 | ||||
|       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 | ||||
|         gl_user = create(:user, email: octocat[:email]) | ||||
| 
 | ||||
|  | @ -130,7 +118,7 @@ RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do | |||
|       end | ||||
| 
 | ||||
|       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.") | ||||
|       end | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| 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(:client) { double } | ||||
|   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 | ||||
|       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 | ||||
|         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 | ||||
|       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 | ||||
|         gl_user = create(:user, email: octocat[:email]) | ||||
| 
 | ||||
|  | @ -169,7 +157,7 @@ RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do | |||
|       end | ||||
| 
 | ||||
|       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') | ||||
|       end | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do | ||||
| RSpec.describe Gitlab::LegacyGithubImport::UserFormatter, feature_category: :importers do | ||||
|   let(:client) { double } | ||||
|   let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } } | ||||
|   let(:gitea_ghost) { { id: -1, login: 'Ghost', email: '' } } | ||||
|  | @ -15,12 +15,6 @@ RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do | |||
|     end | ||||
| 
 | ||||
|     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 | ||||
|         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 | ||||
|       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_inclusion_of(:nuget_duplicates_allowed).in_array([true, false]) } | ||||
|       it { is_expected.to validate_length_of(:maven_duplicate_exception_regex).is_at_most(255) } | ||||
|     end | ||||
| 
 | ||||
|     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.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 | ||||
|       let_it_be(:package_settings) { create(:namespace_package_setting) } | ||||
| 
 | ||||
|  | @ -39,6 +47,50 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | |||
|     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 | ||||
|     using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|  | @ -46,9 +98,14 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | |||
| 
 | ||||
|     context 'package types with package_settings' do | ||||
|       # 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 | ||||
|           let_it_be(:package) { create(format, name: 'foo', version: '1.0.0-beta') } | ||||
|       [ | ||||
|         { format: :maven_package, package_name: 'foo' }, | ||||
|         { 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_setting) { package.project.namespace.package_settings } | ||||
| 
 | ||||
|  | @ -61,7 +118,7 @@ RSpec.describe Namespace::PackageSetting, feature_category: :package_registry do | |||
|           end | ||||
| 
 | ||||
|           with_them do | ||||
|             context "for #{format}" do | ||||
|             context "for #{type[:format]}" do | ||||
|               before do | ||||
|                 package_setting.update!( | ||||
|                   "#{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 | ||||
|       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_user) } | ||||
|     end | ||||
|  | @ -36,12 +37,14 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do | |||
|     end | ||||
| 
 | ||||
|     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_user) } | ||||
|   end | ||||
| 
 | ||||
|   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(:create_group) } | ||||
|     it { is_expected.to be_disallowed(:read_organization_user) } | ||||
|     # All organizations are currently public, and hence they are allowed to be read | ||||
|     # 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, | ||||
|       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 | ||||
| 
 | ||||
|  | @ -44,6 +46,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | |||
|           pypiPackageRequestsForwarding | ||||
|           lockPypiPackageRequestsForwarding | ||||
|           nugetSymbolServerEnabled | ||||
|           terraformModuleDuplicatesAllowed | ||||
|           terraformModuleDuplicateExceptionRegex | ||||
|         } | ||||
|         errors | ||||
|       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['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['terraformModuleDuplicatesAllowed']).to eq(params[:terraform_module_duplicates_allowed]) | ||||
|       expect(package_settings_response['terraformModuleDuplicateExceptionRegex']).to eq(params[:terraform_module_duplicate_exception_regex]) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -115,7 +121,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis | |||
|         lock_npm_package_requests_forwarding: false, | ||||
|         pypi_package_requests_forwarding: nil, | ||||
|         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: { | ||||
|         maven_duplicates_allowed: false, | ||||
|         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, | ||||
|         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' | ||||
|  |  | |||
|  | @ -1937,6 +1937,59 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do | |||
|       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 | ||||
|       it "creates group", :aggregate_failures do | ||||
|         group = attributes_for_group_api request_access_enabled: false | ||||
|  |  | |||
|  | @ -103,7 +103,28 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | |||
|       ) | ||||
|     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 | ||||
|       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 | ||||
|         :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 | ||||
|  | @ -147,7 +168,6 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | |||
| 
 | ||||
|       with_them do | ||||
|         let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } } | ||||
|         let(:headers) { user_headers.merge(workhorse_headers) } | ||||
|         let(:snowplow_gitlab_standard_context) do | ||||
|           { project: project, namespace: project.namespace, user: snowplow_user, | ||||
|             property: 'i_package_terraform_module_user' } | ||||
|  | @ -172,43 +192,73 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: : | |||
|       end | ||||
| 
 | ||||
|       context 'when failed package file save' do | ||||
|         let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } } | ||||
|         let(:headers) { user_headers.merge(workhorse_headers) } | ||||
|         before do | ||||
|           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 | ||||
|           project.add_developer(user) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not create package record', :aggregate_failures do | ||||
|           allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError) | ||||
| 
 | ||||
|           expect { api_request } | ||||
|               .to change { project.packages.count }.by(0) | ||||
|               .and change { Packages::PackageFile.count }.by(0) | ||||
|           expect(response).to have_gitlab_http_status(:error) | ||||
|         end | ||||
| 
 | ||||
|         context 'with an existing package' do | ||||
|           let_it_be_with_reload(:existing_package) do | ||||
|             create(:terraform_module_package, name: 'mymodule/mysystem', version: '1.0.0', project: project) | ||||
|           end | ||||
| 
 | ||||
|           it 'does not create a new 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(:forbidden) | ||||
|           end | ||||
|         it_behaves_like 'not creating a package', :forbidden | ||||
| 
 | ||||
|         context 'when marked as pending_destruction' do | ||||
|             it 'does create a new package' do | ||||
|           before do | ||||
|             existing_package.pending_destruction! | ||||
|           end | ||||
| 
 | ||||
|               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 | ||||
|           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 | ||||
| 
 | ||||
|         context 'when duplicates not allowed' do | ||||
|           it_behaves_like 'not creating a package', :forbidden | ||||
|         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) { '.*non-matching-regex.*' } | ||||
| 
 | ||||
|             it_behaves_like 'not creating a package', :forbidden | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ | |||
|   let!(:user) { create(:user) } | ||||
|   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 | ||||
|     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 | ||||
| 
 | ||||
|   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 | ||||
|     let!(:group) { create(:group) } | ||||
|     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, | ||||
|           pypi_package_requests_forwarding: nil, | ||||
|           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: { | ||||
|           maven_duplicates_allowed: false, | ||||
|           maven_duplicate_exception_regex: 'RELEASE', | ||||
|  | @ -60,7 +62,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: : | |||
|           lock_npm_package_requests_forwarding: true, | ||||
|           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' | ||||
|  | @ -112,7 +116,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: : | |||
|           lock_npm_package_requests_forwarding: true, | ||||
|           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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,10 +2,11 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| 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(:user) { create(:user) } | ||||
|   let_it_be(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } | ||||
|   let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) } | ||||
| 
 | ||||
|   let(:overrides) { {} } | ||||
| 
 | ||||
|  | @ -36,10 +37,72 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | |||
| 
 | ||||
|     context 'package already exists elsewhere' do | ||||
|       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 } | ||||
|       it { expect(subject[:message]).to be 'Access Denied' } | ||||
|       context 'when duplicates not allowed' do | ||||
|         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 | ||||
|         before do | ||||
|  | @ -53,7 +116,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | |||
|     context 'version already exists' do | ||||
|       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.' } | ||||
| 
 | ||||
|       context 'marked as pending_destruction' do | ||||
|  | @ -68,7 +131,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category | |||
|     context 'with empty version' do | ||||
|       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.' } | ||||
|     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_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.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 | ||||
| 
 | ||||
|  | @ -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_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.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 | ||||
| 
 | ||||
|   it_behaves_like 'returning a success' | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue