diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-07 12:08:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-07 12:08:54 +0300 |
commit | dfa6eac07553d5a3f254ee904e4298bd666b410f (patch) | |
tree | a14e04cf628bf1bd28a0c95ed6261461605cfe2e | |
parent | 4be549b5ebd354c69fcd2a09e2a2f642490612cf (diff) |
Add latest changes from gitlab-org/gitlab@master
37 files changed, 998 insertions, 462 deletions
diff --git a/.rubocop_todo/lint/redundant_dir_glob_sort.yml b/.rubocop_todo/lint/redundant_dir_glob_sort.yml index 1e4afd562f0..c992f5647a7 100644 --- a/.rubocop_todo/lint/redundant_dir_glob_sort.yml +++ b/.rubocop_todo/lint/redundant_dir_glob_sort.yml @@ -6,5 +6,4 @@ Lint/RedundantDirGlobSort: - 'config/application.rb' - 'ee/spec/spec_helper.rb' - 'qa/qa/specs/spec_helper.rb' - - 'rubocop/rubocop.rb' - 'spec/spec_helper.rb' @@ -612,7 +612,7 @@ gem 'cvss-suite', '~> 3.0.1', require: 'cvss_suite' # rubocop:todo Gemfile/Missi gem 'arr-pm', '~> 0.0.12' # rubocop:todo Gemfile/MissingFeatureCategory # Remote Development -gem 'devfile', '~> 0.0.23.pre.alpha1' # rubocop:todo Gemfile/MissingFeatureCategory +gem 'devfile', '~> 0.0.24.pre.alpha1', feature_category: :remote_development # Apple plist parsing gem 'CFPropertyList', '~> 3.0.0' # rubocop:todo Gemfile/MissingFeatureCategory diff --git a/Gemfile.checksum b/Gemfile.checksum index af51bee0f08..8b2da2ba7c8 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -111,9 +111,9 @@ {"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"}, {"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"}, {"name":"descendants_tracker","version":"0.0.4","platform":"ruby","checksum":"e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897"}, -{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"arm64-darwin","checksum":"eec9ed97436cd5e9d456e270da979faeecbdeef42ee75ef9b39b45001c2399fb"}, -{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"ruby","checksum":"fba2c679cbafb03da153f73f55a346ae01f4921383575e1f7cda269e7e67e40a"}, -{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"x86_64-linux","checksum":"30e31b39599b7823673f5386f8bf19b7cb2b959c7f34a16704893db437d42094"}, +{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"arm64-darwin","checksum":"4954bf498772dbf534da0638bc59023234fed7423c72c85f21b6504ee4c65482"}, +{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"ruby","checksum":"72bbfc26edb519902d5c68e07188e0a3d699a1866392fa1497e5b7f3abb36600"}, +{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"x86_64-linux","checksum":"d121b1094aa3a24c29592a83c629ee640920e0196711dd06f27b6fa9b1ced609"}, {"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"}, {"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"}, {"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"}, diff --git a/Gemfile.lock b/Gemfile.lock index 9f2b7608041..6f94a4f275e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,7 +442,7 @@ GEM thor (>= 0.19, < 2) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devfile (0.0.23.pre.alpha1) + devfile (0.0.24.pre.alpha1) device_detector (1.0.0) devise (4.8.1) bcrypt (~> 3.0) @@ -1797,7 +1797,7 @@ DEPENDENCIES declarative_policy (~> 1.1.0) deprecation_toolkit (~> 1.5.1) derailed_benchmarks - devfile (~> 0.0.23.pre.alpha1) + devfile (~> 0.0.24.pre.alpha1) device_detector devise (~> 4.8.1) devise-pbkdf2-encryptable (~> 0.0.0)! diff --git a/app/services/bulk_imports/batched_relation_export_service.rb b/app/services/bulk_imports/batched_relation_export_service.rb index 778510f2e35..c7c01c80fbf 100644 --- a/app/services/bulk_imports/batched_relation_export_service.rb +++ b/app/services/bulk_imports/batched_relation_export_service.rb @@ -26,8 +26,6 @@ module BulkImports start_export! export.batches.destroy_all # rubocop: disable Cop/DestroyAll enqueue_batch_exports - rescue StandardError => e - fail_export!(e) ensure FinishBatchedRelationExportWorker.perform_async(export.id) end @@ -81,11 +79,5 @@ module BulkImports def find_or_create_batch(batch_number) export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord end - - def fail_export!(exception) - Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name) - - export.update!(status_event: 'fail_op', error: exception.message.truncate(255)) - end end end diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb index 20795de54f9..3f98d49aa1b 100644 --- a/app/services/bulk_imports/relation_batch_export_service.rb +++ b/app/services/bulk_imports/relation_batch_export_service.rb @@ -19,8 +19,6 @@ module BulkImports upload_compressed_file finish_batch! - rescue StandardError => e - fail_batch!(e) ensure FileUtils.remove_entry(export_path) end @@ -72,12 +70,6 @@ module BulkImports batch.update!(status_event: 'finish', objects_count: exported_objects_count, error: nil) end - def fail_batch!(exception) - Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name) - - batch.update!(status_event: 'fail_op', error: exception.message.truncate(255)) - end - def exported_filepath File.join(export_path, exported_filename) end diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb index 91640496440..6db5ef3e9ec 100644 --- a/app/services/bulk_imports/relation_export_service.rb +++ b/app/services/bulk_imports/relation_export_service.rb @@ -42,8 +42,6 @@ module BulkImports yield export finish_export!(export) - rescue StandardError => e - fail_export!(export, e) end def export_service @@ -87,12 +85,6 @@ module BulkImports export.update!(status_event: 'finish', batched: false, error: nil) end - def fail_export!(export, exception) - Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name) - - export&.update(status_event: 'fail_op', error: exception.class, batched: false) - end - def exported_filepath File.join(export_path, export_service.exported_filename) end diff --git a/app/services/service_desk/custom_email_verifications/update_service.rb b/app/services/service_desk/custom_email_verifications/update_service.rb index 5ef36ce0576..fbd217e3a3e 100644 --- a/app/services/service_desk/custom_email_verifications/update_service.rb +++ b/app/services/service_desk/custom_email_verifications/update_service.rb @@ -8,7 +8,7 @@ module ServiceDesk def execute return error_feature_flag_disabled unless Feature.enabled?(:service_desk_custom_email, project) return error_parameter_missing if settings.blank? || verification.blank? - return error_already_finished if already_finished_and_no_mail? + return error_already_finished if verification.finished? return error_already_failed if already_failed_and_no_mail? verification_error = verify @@ -39,10 +39,6 @@ module ServiceDesk @verification ||= settings.custom_email_verification end - def already_finished_and_no_mail? - verification.finished? && mail.blank? - end - def already_failed_and_no_mail? verification.failed? && mail.blank? end diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb index b3acf3ee01b..87ceb775075 100644 --- a/app/workers/bulk_imports/relation_batch_export_worker.rb +++ b/app/workers/bulk_imports/relation_batch_export_worker.rb @@ -7,7 +7,16 @@ module BulkImports idempotent! data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency feature_category :importers - sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION + sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3 + + sidekiq_retries_exhausted do |job, exception| + batch = BulkImports::ExportBatch.find(job['args'][1]) + portable = batch.export.portable + + Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name) + + batch.update!(status_event: 'fail_op', error: exception.message.truncate(255)) + end def perform(user_id, batch_id) @user = User.find(user_id) diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb index 09600b9363a..168626fee85 100644 --- a/app/workers/bulk_imports/relation_export_worker.rb +++ b/app/workers/bulk_imports/relation_export_worker.rb @@ -10,12 +10,27 @@ module BulkImports loggable_arguments 2, 3 data_consistency :always feature_category :importers - sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION + sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3 worker_resource_boundary :memory + sidekiq_retries_exhausted do |job, exception| + _user_id, portable_id, portable_type, relation, batched = job['args'] + portable = portable(portable_id, portable_type) + + export = portable.bulk_import_exports.find_by_relation(relation) + + Gitlab::ErrorTracking.track_exception(exception, portable_id: portable_id, portable_type: portable.class.name) + + export.update!(status_event: 'fail_op', error: exception.message.truncate(255), batched: batched) + end + + def self.portable(portable_id, portable_class) + portable_class.classify.constantize.find(portable_id) + end + def perform(user_id, portable_id, portable_class, relation, batched = false) user = User.find(user_id) - portable = portable(portable_id, portable_class) + portable = self.class.portable(portable_id, portable_class) config = BulkImports::FileTransfer.config_for(portable) log_extra_metadata_on_done(:relation, relation) @@ -27,11 +42,5 @@ module BulkImports RelationExportService.new(user, portable, relation, jid).execute end end - - private - - def portable(portable_id, portable_class) - portable_class.classify.constantize.find(portable_id) - end end end diff --git a/config/feature_flags/development/oidc_issuer_url.yml b/config/feature_flags/development/oidc_issuer_url.yml new file mode 100644 index 00000000000..e919d1095d1 --- /dev/null +++ b/config/feature_flags/development/oidc_issuer_url.yml @@ -0,0 +1,8 @@ +--- +name: oidc_issuer_url +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135049 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429855 +milestone: '16.6' +type: development +group: group::pipeline security +default_enabled: false diff --git a/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb new file mode 100644 index 00000000000..af589f6337a --- /dev/null +++ b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class InsertNewUltimateTrialPlanIntoPlans < Gitlab::Database::Migration[2.2] + milestone '16.6' + + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + execute <<~SQL + INSERT INTO plans (name, title, created_at, updated_at) + VALUES ('ultimate_trial_paid_customer', 'Ultimate Trial for Paid Customer', current_timestamp, current_timestamp) + SQL + end + + def down + # NOTE: We have a uniqueness constraint for the 'name' column in 'plans' + execute <<~SQL + DELETE FROM plans + WHERE name = 'ultimate_trial_paid_customer' + SQL + end +end diff --git a/db/schema_migrations/20231030154117 b/db/schema_migrations/20231030154117 new file mode 100644 index 00000000000..5380cfa5252 --- /dev/null +++ b/db/schema_migrations/20231030154117 @@ -0,0 +1 @@ +07c4a447b3888046333b0b8fa237411783fc031ea9943520f716ea0c00ed964f
\ No newline at end of file diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md index 455ddaa5e06..09474db1e08 100644 --- a/doc/administration/audit_event_streaming/index.md +++ b/doc/administration/audit_event_streaming/index.md @@ -261,7 +261,11 @@ To delete Google Cloud Logging streaming destinations to a top-level group: ### AWS S3 destinations -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132603) in GitLab 16.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132603) in GitLab 16.6 [with a flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`. Enabled by default. + +FLAG: +On self-managed GitLab, by default this feature is available. To hide the feature per group, an administrator can [disable the feature flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`. +On GitLab.com, this feature is available. Manage AWS S3 destinations for top-level groups. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 91a1460df18..9ef51601468 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13951,6 +13951,18 @@ Represents the YAML definitions for audit events defined in `ee/config/audit_eve | <a id="auditeventdefinitionsavedtodatabase"></a>`savedToDatabase` | [`Boolean!`](#boolean) | Indicates if the event is saved to PostgreSQL database. | | <a id="auditeventdefinitionstreamed"></a>`streamed` | [`Boolean!`](#boolean) | Indicates if the event is streamed to an external destination. | +### `AuditEventStreamingHTTPNamespaceFilter` + +Represents a subgroup or project filter that belongs to an external audit event streaming destination. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="auditeventstreaminghttpnamespacefilterexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination!`](#externalauditeventdestination) | Destination to which the filter belongs. | +| <a id="auditeventstreaminghttpnamespacefilterid"></a>`id` | [`ID!`](#id) | ID of the filter. | +| <a id="auditeventstreaminghttpnamespacefilternamespace"></a>`namespace` | [`Namespace!`](#namespace) | Group or project namespace the filter belongs to. | + ### `AuditEventStreamingHeader` Represents a HTTP header key/value that belongs to an audit streaming destination. @@ -17621,6 +17633,7 @@ Represents an external resource to send audit events to. | <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. (see [Connections](#connections)) | | <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. | | <a id="externalauditeventdestinationname"></a>`name` | [`String!`](#string) | Name of the external destination to send audit events to. | +| <a id="externalauditeventdestinationnamespacefilter"></a>`namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | List of subgroup or project filters for the destination. | | <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. | ### `ExternalIssue` diff --git a/doc/subscriptions/bronze_starter.md b/doc/subscriptions/bronze_starter.md index 3b2ef601136..90e0e77cb9a 100644 --- a/doc/subscriptions/bronze_starter.md +++ b/doc/subscriptions/bronze_starter.md @@ -106,7 +106,7 @@ the tiers are no longer mentioned in GitLab documentation: - [Filtering merge requests](../user/project/merge_requests/index.md#filter-the-list-of-merge-requests) by "approved by" - [Advanced search (Elasticsearch)](../user/search/advanced_search.md) - [Service Desk](../user/project/service_desk/index.md) -- [Storage usage statistics](../user/usage_quotas.md#storage-usage-statistics) +- [Storage usage statistics](../user/usage_quotas.md) The following developer features continue to be available to Starter and Bronze-level subscribers: diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index f3fca8f2def..9b0dbbc5f0e 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -7,11 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Container Scanning **(FREE ALL)** -> - Improved support for FIPS [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263482) in GitLab 13.6 by upgrading `CS_MAJOR_VERSION` from `2` to `3`. -> - Integration with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322656) in GitLab 13.9 by upgrading `CS_MAJOR_VERSION` from `3` to `4`. -> - Integration with Clair [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/321451) in GitLab 13.9. -> - Default container scanning with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61850) in GitLab 14.0. -> - Integration with Grype as an alternative scanner [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326279) in GitLab 14.0. > - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86092) the major analyzer version from `4` to `5` in GitLab 15.0. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86783) from GitLab Ultimate to GitLab Free in 15.0. > - Container Scanning variables that reference Docker [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/357264) in GitLab 15.4. @@ -61,21 +56,21 @@ information directly in the merge request. | Capability | In Free and Premium | In Ultimate | | --- | ------ | ------ | -| [Configure Scanners](#configuration) | Yes | Yes | -| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | Yes | Yes | -| [View JSON Report](#reports-json-format) as a CI job artifact | Yes | Yes | -| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | Yes | Yes | -| Ability to enable container scanning via an MR in the GitLab UI | Yes | Yes | -| [UBI Image Support](#fips-enabled-images) | Yes | Yes | -| Support for Trivy | Yes | Yes | -| Support for Grype | Yes | Yes | +| [Configure Scanners](#configuration) | **{check-circle}** Yes | **{check-circle}** Yes | +| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | **{check-circle}** Yes | **{check-circle}** Yes | +| [View JSON Report](#reports-json-format) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes | +| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes | +| Ability to enable container scanning via an MR in the GitLab UI | **{check-circle}** Yes | **{check-circle}** Yes | +| [UBI Image Support](#fips-enabled-images) | **{check-circle}** Yes | **{check-circle}** Yes | +| Support for Trivy | **{check-circle}** Yes | **{check-circle}** Yes | +| Support for Grype | **{check-circle}** Yes | **{check-circle}** Yes | | Inclusion of GitLab Advisory Database | Limited to the time-delayed content from GitLab [advisories-communities](https://gitlab.com/gitlab-org/advisories-community/) project | Yes - all the latest content from [Gemnasium DB](https://gitlab.com/gitlab-org/security-products/gemnasium-db) | -| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | No | Yes | -| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | No | Yes | -| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | No | Yes | -| Support for the [vulnerability allow list](#vulnerability-allowlisting) | No | Yes | -| [Access to Security Dashboard page](#security-dashboard) | No | Yes | -| [Access to Dependency List page](../dependency_list/index.md) | No | Yes | +| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | **{dotted-circle}** No | **{check-circle}** Yes | +| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | **{dotted-circle}** No | **{check-circle}** Yes | +| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | **{dotted-circle}** No | **{check-circle}** Yes | +| Support for the [vulnerability allow list](#vulnerability-allowlisting) | **{dotted-circle}** No | **{check-circle}** Yes | +| [Access to Security Dashboard page](#security-dashboard) | **{dotted-circle}** No | **{check-circle}** Yes | +| [Access to Dependency List page](../dependency_list/index.md) | **{dotted-circle}** No | **{check-circle}** Yes | ## Prerequisites @@ -278,28 +273,28 @@ including a large number of false positives. | `CS_DOCKERFILE_PATH` | `Dockerfile` | The path to the `Dockerfile` to use for generating remediations. By default, the scanner looks for a file named `Dockerfile` in the root directory of the project. You should configure this variable only if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | All | | `CS_QUIET` | `""` | If set, this variable disables output of the [vulnerabilities table](#container-scanning-job-log-format) in the job log. [Introduced](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning/-/merge_requests/50) in GitLab 15.1. | All | | `CS_TRIVY_JAVA_DB` | `"ghcr.io/aquasecurity/trivy-java-db"` | Specify an alternate location for the [trivy-java-db](https://github.com/aquasecurity/trivy-java-db) vulnerability database. | Trivy | -| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. | All | +| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. | All | ### Supported distributions Support depends on which scanner is used: -| Distribution | Grype | Trivy | -| -------------- | ----- | ----- | -| Alma Linux | | ✅ | -| Alpine Linux | ✅ | ✅ | -| Amazon Linux | ✅ | ✅ | -| BusyBox | ✅ | | -| CentOS | ✅ | ✅ | -| CBL-Mariner | | ✅ | -| Debian | ✅ | ✅ | -| Distroless | ✅ | ✅ | -| Oracle Linux | ✅ | ✅ | -| Photon OS | | ✅ | -| Red Hat (RHEL) | ✅ | ✅ | -| Rocky Linux | | ✅ | -| SUSE | | ✅ | -| Ubuntu | ✅ | ✅ | +| Distribution | Grype | Trivy | +|----------------|------------------------|------------------------| +| Alma Linux | **{dotted-circle}** No | **{check-circle}** Yes | +| Alpine Linux | **{check-circle}** Yes | **{check-circle}** Yes | +| Amazon Linux | **{check-circle}** Yes | **{check-circle}** Yes | +| BusyBox | **{check-circle}** Yes | **{dotted-circle}** No | +| CentOS | **{check-circle}** Yes | **{check-circle}** Yes | +| CBL-Mariner | **{dotted-circle}** No | **{check-circle}** Yes | +| Debian | **{check-circle}** Yes | **{check-circle}** Yes | +| Distroless | **{check-circle}** Yes | **{check-circle}** Yes | +| Oracle Linux | **{check-circle}** Yes | **{check-circle}** Yes | +| Photon OS | **{dotted-circle}** No | **{check-circle}** Yes | +| Red Hat (RHEL) | **{check-circle}** Yes | **{check-circle}** Yes | +| Rocky Linux | **{dotted-circle}** No | **{check-circle}** Yes | +| SUSE | **{dotted-circle}** No | **{check-circle}** Yes | +| Ubuntu | **{check-circle}** Yes | **{check-circle}** Yes | #### FIPS-enabled images @@ -747,24 +742,24 @@ All analyzer images are [updated daily](https://gitlab.com/gitlab-org/security-p The images use data from upstream advisory databases depending on which scanner is used: -| Data Source | Trivy | Grype | -| ------------------------------ | ----- | ----- | -| AlmaLinux Security Advisory | ✅ | ✅ | -| Amazon Linux Security Center | ✅ | ✅ | -| Arch Linux Security Tracker | ✅ | | -| SUSE CVRF | ✅ | ✅ | -| CWE Advisories | ✅ | | -| Debian Security Bug Tracker | ✅ | ✅ | -| GitHub Security Advisory | ✅ | ✅ | -| Go Vulnerability Database | ✅ | | -| CBL-Mariner Vulnerability Data | ✅ | | -| NVD | ✅ | ✅ | -| OSV | ✅ | | -| Red Hat OVAL v2 | ✅ | ✅ | -| Red Hat Security Data API | ✅ | ✅ | -| Photon Security Advisories | ✅ | | -| Rocky Linux UpdateInfo | ✅ | | -| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | ✅ | ✅ | +| Data Source | Trivy | Grype | +|----------------------------------------------------------------|------------------------|------------------------| +| AlmaLinux Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes | +| Amazon Linux Security Center | **{check-circle}** Yes | **{check-circle}** Yes | +| Arch Linux Security Tracker | **{check-circle}** Yes | **{dotted-circle}** No | +| SUSE CVRF | **{check-circle}** Yes | **{check-circle}** Yes | +| CWE Advisories | **{check-circle}** Yes | **{dotted-circle}** No | +| Debian Security Bug Tracker | **{check-circle}** Yes | **{check-circle}** Yes | +| GitHub Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes | +| Go Vulnerability Database | **{check-circle}** Yes | **{dotted-circle}** No | +| CBL-Mariner Vulnerability Data | **{check-circle}** Yes | **{dotted-circle}** No | +| NVD | **{check-circle}** Yes | **{check-circle}** Yes | +| OSV | **{check-circle}** Yes | **{dotted-circle}** No | +| Red Hat OVAL v2 | **{check-circle}** Yes | **{check-circle}** Yes | +| Red Hat Security Data API | **{check-circle}** Yes | **{check-circle}** Yes | +| Photon Security Advisories | **{check-circle}** Yes | **{dotted-circle}** No | +| Rocky Linux UpdateInfo | **{check-circle}** Yes | **{dotted-circle}** No | +| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | **{check-circle}** Yes | **{check-circle}** Yes | In addition to the sources provided by these scanners, GitLab maintains the following vulnerability databases: diff --git a/doc/user/packages/container_registry/reduce_container_registry_storage.md b/doc/user/packages/container_registry/reduce_container_registry_storage.md index 2af16dcc85a..8c4f25af2e1 100644 --- a/doc/user/packages/container_registry/reduce_container_registry_storage.md +++ b/doc/user/packages/container_registry/reduce_container_registry_storage.md @@ -15,14 +15,61 @@ if you add a large number of images or tags: You should delete unnecessary images and tags and set up a [cleanup policy](#cleanup-policy) to automatically manage your container registry usage. -## Check Container Registry storage use +## Check Container Registry storage use **(FREE SAAS)** The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages. -This page includes the [Container Registry usage](../../usage_quotas.md#container-registry-usage), which is only available on GitLab.com. Measuring usage is only possible on the new version of the GitLab Container Registry backed by a metadata database, which is [available on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/5523) since GitLab 15.7. For information on the planned availability for self-managed instances, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521). +## How container registry usage is calculated + +Image layers stored in the Container Registry are deduplicated at the root namespace level. + +An image is only counted once if: + +- You tag the same image more than once in the same repository. +- You tag the same image across distinct repositories under the same root namespace. + +An image layer is only counted once if: + +- You share the image layer across multiple images in the same container repository, project, or group. +- You share the image layer across different repositories. + +Only layers that are referenced by tagged images are accounted for. Untagged images and any layers +referenced exclusively by them are subject to [online garbage collection](../container_registry/delete_container_registry_images.md#garbage-collection). +Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period. + +Image layers are stored on the storage backend in the original (usually compressed) format. This +means that the measured size for any given image layer should match the size displayed on the +corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest). + +Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace. + +### Delayed refresh + +It is not possible to calculate container registry usage +with maximum precision in real time for extremely large namespaces (about 1% of namespaces). +To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism. +See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details. + +If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method. +In the delayed method, the displayed usage size is the sum of **all** unique image layers +in the namespace. Untagged image layers are not ignored. As a result, +the displayed usage size might not change significantly after deleting tags. Instead, +the size value only changes when: + +- An automated [garbage collection process](../container_registry/delete_container_registry_images.md#garbage-collection) + runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run + is scheduled to start 24 hours later. During that run, images that were previously tagged + are analyzed and their layers deleted if not referenced by any other tagged image. + If any layers are deleted, the namespace usage is updated. +- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision. + As usage for namespaces shrinks to be under the [limits](../../../user/usage_quotas.md#namespace-storage-limit), + the measurement switches automatically from delayed to precise usage measurement. + There is no place in the UI to determine which measurement method is being used, + but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this. + ## Cleanup policy > - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/218737) from "expiration policy" to "cleanup policy" in GitLab 13.2. diff --git a/doc/user/read_only_namespaces.md b/doc/user/read_only_namespaces.md index 5b302d976dd..d5697ec5a94 100644 --- a/doc/user/read_only_namespaces.md +++ b/doc/user/read_only_namespaces.md @@ -27,7 +27,7 @@ To restore a namespace to its standard state, you can: - [Purchase a paid tier](https://about.gitlab.com/pricing/). - For exceeded storage quota: - [Purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer). - - [Manage your storage usage](usage_quotas.md#manage-your-storage-usage). + - [Manage your storage usage](usage_quotas.md#manage-storage-usage). ## Restricted actions diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 305a46e1f15..7dea2b97249 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -5,7 +5,7 @@ group: Utilization info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- -# Storage usage quota **(FREE ALL)** +# Storage **(FREE ALL)** Storage usage statistics are available for projects and namespaces. You can use that information to manage storage usage within the applicable quotas. @@ -13,8 +13,8 @@ manage storage usage within the applicable quotas. Statistics include: - Storage usage across projects in a namespace. -- Storage usage that exceeds the storage quota. -- Available purchased storage. +- Storage usage that exceeds the storage SaaS limit or [self-managed storage quota](../administration/settings/account_and_limit_settings.md#repository-size-limit). +- Available purchased storage for SaaS. Storage and network usage are calculated with the binary measurement system (1024 unit multiples). Storage usage is displayed in kibibytes (KiB), mebibytes (MiB), @@ -30,87 +30,33 @@ you might see references to `KB`, `MB`, and `GB` in the UI and documentation. Prerequisites: - To view storage usage for a project, you must have at least the Maintainer role for the project or Owner role for the namespace. -- To view storage usage for a namespace, you must have the Owner role for the namespace. +- To view storage usage for a group namespace, you must have the Owner role for the namespace. 1. On the left sidebar, select **Search or go to** and find your project or group. 1. On the left sidebar, select **Settings > Usage Quotas**. -1. Select the **Storage** tab. +1. Select the **Storage** tab to see namespace storage usage. +1. To view storage usage for a project, select one of the projects from the table at the bottom of the **Storage** tab of the **Usage Quotas** page. -Select any title to view details. The information on this page -is updated every 90 minutes. +The information on the **Usage Quotas** page is updated every 90 minutes. If your namespace shows `'Not applicable.'`, push a commit to any project in the namespace to recalculate the storage. -### Container Registry usage **(FREE SAAS)** +### View project fork storage usage **(FREE SAAS)** -Container Registry usage is available only for GitLab.com. This feature requires a -[new version](https://about.gitlab.com/blog/2022/04/12/next-generation-container-registry/) -of the GitLab Container Registry. To learn about the proposed release for self-managed -installations, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521). - -#### How container registry usage is calculated - -Image layers stored in the Container Registry are deduplicated at the root namespace level. - -An image is only counted once if: - -- You tag the same image more than once in the same repository. -- You tag the same image across distinct repositories under the same root namespace. - -An image layer is only counted once if: - -- You share the image layer across multiple images in the same container repository, project, or group. -- You share the image layer across different repositories. - -Only layers that are referenced by tagged images are accounted for. Untagged images and any layers -referenced exclusively by them are subject to [online garbage collection](packages/container_registry/delete_container_registry_images.md#garbage-collection). -Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period. - -Image layers are stored on the storage backend in the original (usually compressed) format. This -means that the measured size for any given image layer should match the size displayed on the -corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest). - -Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace. - -#### Delayed refresh - -It is not possible to calculate [container registry usage](#container-registry-usage) -with maximum precision in real time for extremely large namespaces (about 1% of namespaces). -To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism. -See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details. - -If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method. -In the delayed method, the displayed usage size is the sum of **all** unique image layers -in the namespace. Untagged image layers are not ignored. As a result, -the displayed usage size might not change significantly after deleting tags. Instead, -the size value only changes when: - -- An automated [garbage collection process](packages/container_registry/delete_container_registry_images.md#garbage-collection) - runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run - is scheduled to start 24 hours later. During that run, images that were previously tagged - are analyzed and their layers deleted if not referenced by any other tagged image. - If any layers are deleted, the namespace usage is updated. -- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision. - As usage for namespaces shrinks to be under the [limits](#namespace-storage-limit), - the measurement switches automatically from delayed to precise usage measurement. - There is no place in the UI to determine which measurement method is being used, - but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this. +A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size. -### Storage usage statistics +To view the amount of namespace storage the fork has used: -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68898) project-level graph in GitLab 14.4 [with a flag](../administration/feature_flags.md) named `project_storage_ui`. Disabled by default. -> - Enabled on GitLab.com in GitLab 14.4. -> - Enabled on self-managed in GitLab 14.5. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71270) in GitLab 14.5. +1. On the left sidebar, select **Search or go to** and find your project or group. +1. On the left sidebar, select **Settings > Usage Quotas**. +1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk. -The following storage usage statistics are available to a maintainer: +The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki. -- Total namespace storage used: Total amount of storage used across projects in this namespace. -- Total excess storage used: Total amount of storage used that exceeds their allocated storage. -- Purchased storage available: Total storage that has been purchased but is not yet used. +The cost factor does not apply to private forks in namespaces on the Free plan. -## Manage your storage usage +## Manage storage usage To manage your storage, if you are a namespace Owner you can [purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer). @@ -126,14 +72,16 @@ Depending on your role, you can also use the following methods to manage or redu To automate storage usage analysis and management, see the [storage management automation](storage_management_automation.md) documentation. -## Manage your transfer usage +## Set usage quotas **(FREE SELF)** + +There are no application limits on the amount of storage and transfer for self-managed instances. The administrators are responsible for the underlying infrastructure costs. Administrators can set [repository size limits](../administration/settings/account_and_limit_settings.md#repository-size-limit) to manage your repositories’ size. -Depending on your role, to manage your transfer usage you can [reduce Container Registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md). +## Storage limits **(FREE SAAS)** -## Project storage limit +### Project storage limit -Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage. -After namespace-level storage limits are applied, the project limit is removed. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both. +Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage. Limits on project storage +will be removed before limits are applied to GitLab SaaS namespace storage in the future. When a project's repository and LFS reaches the quota, the project is set to a read-only state. You cannot push changes to a read-only project. To monitor the size of each @@ -141,7 +89,7 @@ repository in a namespace, including a breakdown for each project, [view storage usage](#view-storage-usage). To allow a project's repository and LFS to exceed the free quota you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage). -### Excess storage usage +#### Excess storage usage Excess storage usage is the amount that a project's repository and LFS exceeds the [project storage limit](#project-storage-limit). If no purchased storage is available the project is set to a read-only state. You cannot push changes to a read-only project. @@ -185,12 +133,19 @@ available decreases. All projects no longer have the read-only status because 40 | Yellow | 5 GiB | 0 GiB | 10 GiB | Not read-only | | **Totals** | **45 GiB** | **10 GiB** | - | - | -## Namespace storage limit +### Namespace storage limit **(FREE SAAS)** -Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/). +GitLab plans to enforce a storage limit for namespaces on GitLab SaaS. For more information, see +the FAQs for the following tiers: -After namespace storage limits are enforced, view them in the **Usage quotas** page. -For more information about the namespace storage limit enforcement, see the FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tiers. +- [Free tier](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier). +- [Premium and Ultimate](https://about.gitlab.com/pricing/faq-paid-storage-transfer/). + +Namespaces on GitLab SaaS have a [10 GiB project limit](#project-storage-limit) with a soft limit on +namespace storage. Soft storage limits are limits that have not yet been enforced by GitLab, and will become +hard limits after namespace storage limits apply. To avoid your namespace from becoming +[read-only](../user/read_only_namespaces.md) after namespace storage limits apply, +you should ensure that your namespace storage adheres to the soft storage limit. Namespace storage limits do not apply to self-managed deployments, but administrators can [manage the repository size](../administration/settings/account_and_limit_settings.md#repository-size-limit). @@ -209,13 +164,13 @@ If your total namespace storage exceeds the available namespace storage quota, a To notify you that you have nearly exceeded your namespace storage quota: -- In the command-line interface, a notification displays after each `git push` action when you've reached 95% and 100% of your namespace storage quota. -- In the GitLab UI, a notification displays when you've reached 75%, 95%, and 100% of your namespace storage quota. +- In the command-line interface, a notification displays after each `git push` action when your namespace has reached between 95% and 100%+ of your namespace storage quota. +- In the GitLab UI, a notification displays when your namespace has reached between 75% and 100%+ of your namespace storage quota. - GitLab sends an email to members with the Owner role to notify them when namespace storage usage is at 70%, 85%, 95%, and 100%. To prevent exceeding the namespace storage limit, you can: -- [Manage your storage usage](#manage-your-storage-usage). +- [Manage your storage usage](#manage-storage-usage). - If you meet the eligibility requirements, you can apply for: - [GitLab for Education](https://about.gitlab.com/solutions/education/join/) - [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/) @@ -225,16 +180,8 @@ To prevent exceeding the namespace storage limit, you can: - [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing/), which include higher limits and features to enable growing teams to ship faster without sacrificing on quality. - [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) for more information about your options. -### View project fork storage usage - -A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size. - -To view the amount of namespace storage the fork has used: - -1. On the left sidebar, select **Search or go to** and find your project or group. -1. On the left sidebar, select **Settings > Usage Quotas**. -1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk. - -The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki. +## Related Topics -The cost factor does not apply to private forks in namespaces on the Free plan. +- [Automate storage management](storage_management_automation.md) +- [Purchase storage and transfer](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) +- [Transfer usage](packages/container_registry/reduce_container_registry_data_transfer.md) diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index 2c61b0fd236..90db9d13d85 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -25,7 +25,7 @@ module Gitlab def reserved_claims super.merge({ - iss: Settings.gitlab.base_url, + iss: Feature.enabled?(:oidc_issuer_url) ? Gitlab.config.gitlab.url : Settings.gitlab.base_url, sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}", aud: aud }.compact) diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index 1ce0a44e37f..b486ddb8e76 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -8,7 +8,8 @@ module Gitlab include ::Gitlab::Database::MigrationHelpers include ::Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers - ALLOWED_TABLES = %w[audit_events web_hook_logs].freeze + ALLOWED_TABLES = %w[audit_events web_hook_logs merge_request_diff_files merge_request_diff_commits].freeze + ERROR_SCOPE = 'table partitioning' MIGRATION_CLASS_NAME = "::#{module_parent_name}::BackfillPartitionedTable" @@ -16,6 +17,60 @@ module Gitlab BATCH_INTERVAL = 2.minutes.freeze BATCH_SIZE = 50_000 SUB_BATCH_SIZE = 2_500 + PARTITION_BUFFER = 6 + MIN_ID = 1 + + # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a int/bigint column. + # One partition is created per partition_size between 1 and MAX(column_name). Also installs a trigger on + # the original table to copy writes into the partitioned table. To copy over historic data from before creation + # of the partitioned table, use the `enqueue_partitioning_data_migration` helper in a post-deploy migration. + # Note: If the original table is empty the system creates 6 partitions in the new table. + # + # A copy of the original table is required as PG currently does not support partitioning existing tables. + # + # Example: + # + # partition_table_by_int_range :merge_request_diff_commits, :merge_request_diff_id, partition_size: 500, primary_key: ['merge_request_diff_id', 'relative_order'] + # + # Options are: + # :partition_size - a int specifying the partition size + # :primary_key - a array specifying the primary query of the new table + # + # Note: The system always adds a buffer of 6 partitions. + def partition_table_by_int_range(table_name, column_name, partition_size:, primary_key:) + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode! + + assert_table_is_allowed(table_name) + + assert_not_in_transaction_block(scope: ERROR_SCOPE) + + current_primary_key = Array.wrap(connection.primary_key(table_name)) + raise "primary key not defined for #{table_name}" if current_primary_key.blank? + + partition_column = find_column_definition(table_name, column_name) + raise "partition column #{column_name} does not exist on #{table_name}" if partition_column.nil? + + primary_key = Array.wrap(primary_key).map(&:to_s) + raise "the partition column must be part of the primary key" unless primary_key.include?(column_name.to_s) + + primary_key_objects = connection.columns(table_name).select { |column| primary_key.include?(column.name) } + + raise 'partition_size must be greater than 1' unless partition_size > 1 + + max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do + define_batchable_model(table_name, connection: connection).maximum(column_name) || partition_size * PARTITION_BUFFER + end + end + + partitioned_table_name = make_partitioned_table_name(table_name) + + with_lock_retries do + create_range_id_partitioned_copy(table_name, partitioned_table_name, partition_column, primary_key_objects) + create_int_range_partitions(partitioned_table_name, partition_size, MIN_ID, max_id) + create_trigger_to_sync_tables(table_name, partitioned_table_name, current_primary_key) + end + end # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column. # One partition is created per month between the given `min_date` and `max_date`. Also installs a trigger on @@ -332,6 +387,34 @@ module Gitlab connection.columns(table).find { |c| c.name == column.to_s } end + def create_range_id_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_keys) + if table_exists?(partitioned_table_name) + Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \ + " (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} " + return + end + + tmp_partitioning_column_name = "#{partition_column.name}_tmp" + + temporary_columns = primary_keys.map { |key| "#{key.name}_tmp" }.join(", ") + temporary_columns_statement = build_temporary_columns_statement(primary_keys) + + transaction do + execute(<<~SQL) + CREATE TABLE #{partitioned_table_name} ( + LIKE #{source_table_name} INCLUDING ALL EXCLUDING INDEXES, + #{temporary_columns_statement}, + PRIMARY KEY (#{temporary_columns}) + ) PARTITION BY RANGE (#{tmp_partitioning_column_name}) + SQL + + primary_keys.each do |key| + remove_column(partitioned_table_name, key.name) + rename_column(partitioned_table_name, "#{key.name}_tmp", key.name) + end + end + end + def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key) if table_exists?(partitioned_table_name) Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \ @@ -382,6 +465,20 @@ module Gitlab end end + def create_int_range_partitions(table_name, partition_size, min_id, max_id) + lower_bound = min_id + upper_bound = min_id + partition_size + + end_id = max_id + PARTITION_BUFFER * partition_size # Adds a buffer of 6 partitions + + while lower_bound < end_id + create_range_partition_safely("#{table_name}_#{lower_bound}", table_name, lower_bound, upper_bound) + + lower_bound += partition_size + upper_bound += partition_size + end + end + def to_sql_date_literal(date) connection.quote(date.strftime('%Y-%m-%d')) end @@ -411,19 +508,23 @@ module Gitlab return end + unique_key = Array.wrap(unique_key) + delimiter = ",\n " column_names = connection.columns(partitioned_table_name).map(&:name) set_statements = build_set_statements(column_names, unique_key) insert_values = column_names.map { |name| "NEW.#{name}" } + delete_where_statement = unique_key.map { |unique_key| "#{unique_key} = OLD.#{unique_key}" }.join(' AND ') + update_where_statement = unique_key.map { |unique_key| "#{partitioned_table_name}.#{unique_key} = NEW.#{unique_key}" }.join(' AND ') create_trigger_function(name, replace: false) do <<~SQL IF (TG_OP = 'DELETE') THEN - DELETE FROM #{partitioned_table_name} where #{unique_key} = OLD.#{unique_key}; + DELETE FROM #{partitioned_table_name} where #{delete_where_statement}; ELSIF (TG_OP = 'UPDATE') THEN UPDATE #{partitioned_table_name} SET #{set_statements.join(delimiter)} - WHERE #{partitioned_table_name}.#{unique_key} = NEW.#{unique_key}; + WHERE #{update_where_statement}; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO #{partitioned_table_name} (#{column_names.join(delimiter)}) VALUES (#{insert_values.join(delimiter)}); @@ -433,8 +534,16 @@ module Gitlab end end + def build_temporary_columns_statement(columns) + columns.map do |column| + type = column.name == 'id' || column.name.end_with?('_id') ? 'bigint' : column.sql_type + + "#{column.name}_tmp #{type} NOT NULL" + end.join(", ") + end + def build_set_statements(column_names, unique_key) - column_names.reject { |name| name == unique_key }.map { |name| "#{name} = NEW.#{name}" } + column_names.reject { |name| unique_key.include?(name) }.map { |name| "#{name} = NEW.#{name}" } end def create_sync_trigger(table_name, trigger_name, function_name) diff --git a/lib/gitlab/middleware/path_traversal_check.rb b/lib/gitlab/middleware/path_traversal_check.rb index 96d0a1c8ff9..6fef247b708 100644 --- a/lib/gitlab/middleware/path_traversal_check.rb +++ b/lib/gitlab/middleware/path_traversal_check.rb @@ -5,6 +5,28 @@ module Gitlab class PathTraversalCheck PATH_TRAVERSAL_MESSAGE = 'Potential path traversal attempt detected' + EXCLUDED_EXACT_PATHS = %w[/search].freeze + EXCLUDED_PATH_PREFIXES = %w[/search/].freeze + + EXCLUDED_API_PATHS = %w[/search].freeze + EXCLUDED_PROJECT_API_PATHS = %w[/search].freeze + EXCLUDED_GROUP_API_PATHS = %w[/search].freeze + + API_PREFIX = %r{/api/[^/]+} + API_SUFFIX = %r{(?:\.[^/]+)?} + + EXCLUDED_API_PATHS_REGEX = [ + EXCLUDED_API_PATHS.map do |path| + %r{\A#{API_PREFIX}#{path}#{API_SUFFIX}\z} + end.freeze, + EXCLUDED_PROJECT_API_PATHS.map do |path| + %r{\A#{API_PREFIX}/projects/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z} + end.freeze, + EXCLUDED_GROUP_API_PATHS.map do |path| + %r{\A#{API_PREFIX}/groups/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z} + end.freeze + ].flatten.freeze + def initialize(app) @app = app end @@ -14,7 +36,8 @@ module Gitlab log_params = {} execution_time = measure_execution_time do - check(env, log_params) + request = ::Rack::Request.new(env.dup) + check(request, log_params) unless excluded?(request) end log_params[:duration_ms] = execution_time.round(5) if execution_time @@ -37,18 +60,25 @@ module Gitlab end end - def check(env, log_params) - request = ::Rack::Request.new(env.dup) - fullpath = request.fullpath - decoded_fullpath = CGI.unescape(fullpath) + def check(request, log_params) + decoded_fullpath = CGI.unescape(request.fullpath) ::Gitlab::PathTraversal.check_path_traversal!(decoded_fullpath, skip_decoding: true) - rescue ::Gitlab::PathTraversal::PathTraversalAttackError log_params[:method] = request.request_method - log_params[:fullpath] = fullpath + log_params[:fullpath] = request.fullpath log_params[:message] = PATH_TRAVERSAL_MESSAGE end + def excluded?(request) + path = request.path + + return true if path.in?(EXCLUDED_EXACT_PATHS) + return true if EXCLUDED_PATH_PREFIXES.any? { |p| path.start_with?(p) } + return true if EXCLUDED_API_PATHS_REGEX.any? { |r| path.match?(r) } + + false + end + def log(payload) Gitlab::AppLogger.warn( payload.merge(class_name: self.class.name) diff --git a/package.json b/package.json index 799755386db..83ffbb04323 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/svgs": "3.69.0", - "@gitlab/ui": "67.5.1", + "@gitlab/ui": "67.5.2", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20231004090414", "@mattiasbuelens/web-streams-adapter": "^0.1.0", diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 42882966b85..708be988f3a 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,7 +1,11 @@ # rubocop:disable Naming/FileName # frozen_string_literal: true +# Load ActiveSupport to ensure that core extensions like `Enumerable#exclude?` +# are available in cop rules like `Performance/CollectionLiteralInLoop`. +require 'active_support/all' + # Auto-require all cops under `rubocop/cop/**/*.rb` -Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each { |file| require file } +Dir[File.join(__dir__, 'cop', '**', '*.rb')].each { |file| require file } # rubocop:enable Naming/FileName diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index d45d8cacb88..c2ced10620b 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -33,14 +33,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do describe '#payload' do subject(:payload) { ci_job_jwt_v2.payload } - it 'has correct values for the standard JWT attributes' do - aggregate_failures do - expect(payload[:iss]).to eq(Settings.gitlab.base_url) - expect(payload[:aud]).to eq(Settings.gitlab.base_url) - expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") - end - end - it 'includes user identities when enabled' do expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(true) identities = payload[:user_identities].map { |identity| identity.slice(:extern_uid, :provider) } @@ -53,6 +45,34 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do expect(payload).not_to include(:user_identities) end + context 'when oidc_issuer_url is disabled' do + before do + stub_feature_flags(oidc_issuer_url: false) + end + + it 'has correct values for the standard JWT attributes' do + aggregate_failures do + expect(payload[:iss]).to eq(Settings.gitlab.base_url) + expect(payload[:aud]).to eq(Settings.gitlab.base_url) + expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") + end + end + end + + context 'when oidc_issuer_url is enabled' do + before do + stub_feature_flags(oidc_issuer_url: true) + end + + it 'has correct values for the standard JWT attributes' do + aggregate_failures do + expect(payload[:iss]).to eq(Gitlab.config.gitlab.url) + expect(payload[:aud]).to eq(Settings.gitlab.base_url) + expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") + end + end + end + context 'when given an aud' do let(:aud) { 'AWS' } diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 182b6960384..31c669ff330 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -2,6 +2,173 @@ require 'spec_helper' +RSpec.shared_examples "a measurable object" do + context 'when the table is not allowed' do + let(:source_table) { :_test_this_table_is_not_allowed } + + it 'raises an error' do + expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original + + expect do + subject + end.to raise_error(/#{source_table} is not allowed for use/) + end + end + + context 'when run inside a transaction block' do + it 'raises an error' do + expect(migration).to receive(:transaction_open?).and_return(true) + + expect do + subject + end.to raise_error(/can not be run inside a transaction/) + end + end + + context 'when the given table does not have a primary key' do + it 'raises an error' do + migration.execute(<<~SQL) + ALTER TABLE #{source_table} + DROP CONSTRAINT #{source_table}_pkey + SQL + + expect do + subject + end.to raise_error(/primary key not defined for #{source_table}/) + end + end + + it 'creates the partitioned table with the same non-key columns' do + subject + + copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key) + original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key) + + expect(copied_columns).to match_array(original_columns) + end + + it 'removes the default from the primary key column' do + subject + + pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } + + expect(pk_column.default_function).to be_nil + end + + describe 'constructing the partitioned table' do + it 'creates a table partitioned by the proper column' do + subject + + expect(connection.table_exists?(partitioned_table)).to be(true) + expect(connection.primary_key(partitioned_table)).to eq(new_primary_key) + + expect_table_partitioned_by(partitioned_table, [partition_column_name]) + end + + it 'requires the migration helper to be run in DDL mode' do + expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!) + + subject + + expect(connection.table_exists?(partitioned_table)).to be(true) + expect(connection.primary_key(partitioned_table)).to eq(new_primary_key) + + expect_table_partitioned_by(partitioned_table, [partition_column_name]) + end + + it 'changes the primary key datatype to bigint' do + subject + + pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } + + expect(pk_column.sql_type).to eq('bigint') + end + + it 'removes the default from the primary key column' do + subject + + pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } + + expect(pk_column.default_function).to be_nil + end + + it 'creates the partitioned table with the same non-key columns' do + subject + + copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key) + original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key) + + expect(copied_columns).to match_array(original_columns) + end + end + + describe 'keeping data in sync with the partitioned table' do + before do + partitioned_model.primary_key = :id + partitioned_model.table_name = partitioned_table + end + + it 'creates a trigger function on the original table' do + expect_function_not_to_exist(function_name) + expect_trigger_not_to_exist(source_table, trigger_name) + + subject + + expect_function_to_exist(function_name) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + end + + it 'syncs inserts to the partitioned tables' do + subject + + expect(partitioned_model.count).to eq(0) + + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp) + + expect(partitioned_model.count).to eq(2) + expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes) + expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) + end + + it 'syncs updates to the partitioned tables' do + subject + + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp) + + expect(partitioned_model.count).to eq(2) + + first_copy = partitioned_model.find(first_record.id) + second_copy = partitioned_model.find(second_record.id) + + expect(first_copy.attributes).to eq(first_record.attributes) + expect(second_copy.attributes).to eq(second_record.attributes) + + first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3) + + expect(partitioned_model.count).to eq(2) + expect(first_copy.reload.attributes).to eq(first_record.attributes) + expect(second_copy.reload.attributes).to eq(second_record.attributes) + end + + it 'syncs deletes to the partitioned tables' do + subject + + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp) + + expect(partitioned_model.count).to eq(2) + + first_record.destroy! + + expect(partitioned_model.count).to eq(1) + expect(partitioned_model.find_by_id(first_record.id)).to be_nil + expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) + end + end +end + RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do include Database::PartitioningHelpers include Database::TriggerHelpers @@ -18,6 +185,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe let(:partitioned_table) { :_test_migration_partitioned_table } let(:function_name) { :_test_migration_function_name } let(:trigger_name) { :_test_migration_trigger_name } + let(:partition_column2) { 'external_id' } let(:partition_column) { 'created_at' } let(:min_date) { Date.new(2019, 12) } let(:max_date) { Date.new(2020, 3) } @@ -29,6 +197,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe migration.create_table source_table do |t| t.string :name, null: false t.integer :age, null: false + t.integer partition_column2 t.datetime partition_column t.datetime :updated_at end @@ -127,33 +296,190 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end - describe '#partition_table_by_date' do - let(:partition_column) { 'created_at' } + describe '#partition_table_by_int_range' do let(:old_primary_key) { 'id' } - let(:new_primary_key) { [old_primary_key, partition_column] } + let(:new_primary_key) { ['id', partition_column2] } + let(:partition_column_name) { partition_column2 } + let(:partitioned_model) { Class.new(ActiveRecord::Base) } + let(:timestamp) { Time.utc(2019, 12, 1, 12).round } + let(:partition_size) { 500 } - context 'when the table is not allowed' do - let(:source_table) { :_test_this_table_is_not_allowed } + subject { migration.partition_table_by_int_range(source_table, partition_column2, partition_size: partition_size, primary_key: ['id', partition_column2]) } - it 'raises an error' do - expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original + include_examples "a measurable object" - expect do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/#{source_table} is not allowed for use/) + context 'simulates the merge_request_diff_commits migration' do + let(:table_name) { '_test_merge_request_diff_commits' } + let(:partition_column_name) { 'relative_order' } + let(:partition_size) { 2 } + let(:partitions) do + { + '1' => %w[1 3], + '3' => %w[3 5], + '5' => %w[5 7], + '7' => %w[7 9], + '9' => %w[9 11], + '11' => %w[11 13] + } + end + + let(:buffer_partitions) do + { + '13' => %w[13 15], + '15' => %w[15 17], + '17' => %w[17 19], + '19' => %w[19 21], + '21' => %w[21 23], + '23' => %w[23 25] + } + end + + let(:new_table_defition) do + { + new_path: { default: 'test', null: true, sql_type: 'text' }, + merge_request_diff_id: { default: nil, null: false, sql_type: 'bigint' }, + relative_order: { default: nil, null: false, sql_type: 'integer' } + } + end + + let(:primary_key) { %w[merge_request_diff_id relative_order] } + + before do + migration.create_table table_name, primary_key: primary_key do |t| + t.integer :merge_request_diff_id, null: false, default: 1 + t.integer :relative_order, null: false + t.text :new_path, null: true, default: 'test' + end + + source_model.table_name = table_name + end + + it 'creates the partitions' do + migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key) + + expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions)) + end + + it 'creates a composite primary key' do + migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key) + + expect(connection.primary_key(:_test_migration_partitioned_table)).to eql(%w[merge_request_diff_id relative_order]) + end + + it 'applies the correct column schema for the new table' do + migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key) + + columns = connection.columns(:_test_migration_partitioned_table) + + columns.each do |column| + column_name = column.name.to_sym + + expect(column.default).to eql(new_table_defition[column_name][:default]) + expect(column.null).to eql(new_table_defition[column_name][:null]) + expect(column.sql_type).to eql(new_table_defition[column_name][:sql_type]) + end + end + + it 'creates multiple partitions' do + migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: 500, primary_key: primary_key) + + expect_range_partitions_for(partitioned_table, { + '1' => %w[1 501], + '501' => %w[501 1001], + '1001' => %w[1001 1501], + '1501' => %w[1501 2001], + '2001' => %w[2001 2501], + '2501' => %w[2501 3001], + '3001' => %w[3001 3501], + '3501' => %w[3501 4001], + '4001' => %w[4001 4501], + '4501' => %w[4501 5001], + '5001' => %w[5001 5501], + '5501' => %w[5501 6001] + }) + end + + context 'when the table is not empty' do + before do + source_model.create!(merge_request_diff_id: 1, relative_order: 7, new_path: 'new_path') + end + + let(:partition_size) { 2 } + + let(:partitions) do + { + '1' => %w[1 3], + '3' => %w[3 5], + '5' => %w[5 7] + } + end + + let(:buffer_partitions) do + { + '7' => %w[7 9], + '9' => %w[9 11], + '11' => %w[11 13], + '13' => %w[13 15], + '15' => %w[15 17], + '17' => %w[17 19] + } + end + + it 'defaults the min_id to 1 and the max_id to 7' do + migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key) + + expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions)) + end end end - context 'when run inside a transaction block' do + context 'when an invalid partition column is given' do + let(:invalid_column) { :_this_is_not_real } + it 'raises an error' do - expect(migration).to receive(:transaction_open?).and_return(true) + expect do + migration.partition_table_by_int_range(source_table, invalid_column, partition_size: partition_size, primary_key: ['id']) + end.to raise_error(/partition column #{invalid_column} does not exist/) + end + end + + context 'when partition_size is less than 1' do + let(:partition_size) { 1 } + it 'raises an error' do expect do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/can not be run inside a transaction/) + subject + end.to raise_error(/partition_size must be greater than 1/) end end + context 'when the partitioned table already exists' do + before do + migration.send(:create_range_id_partitioned_copy, source_table, + migration.send(:make_partitioned_table_name, source_table), + connection.columns(source_table).find { |c| c.name == partition_column2 }, + connection.columns(source_table).select { |c| new_primary_key.include?(c.name) }) + end + + it 'raises an error' do + expect(Gitlab::AppLogger).to receive(:warn).with(/Partitioned table not created because it already exists/) + expect { subject }.not_to raise_error + end + end + end + + describe '#partition_table_by_date' do + let(:partition_column) { 'created_at' } + let(:old_primary_key) { 'id' } + let(:new_primary_key) { [old_primary_key, partition_column] } + let(:partition_column_name) { 'created_at' } + let(:partitioned_model) { Class.new(ActiveRecord::Base) } + let(:timestamp) { Time.utc(2019, 12, 1, 12).round } + + subject { migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date } + + include_examples "a measurable object" + context 'when the the max_date is less than the min_date' do let(:max_date) { Time.utc(2019, 6) } @@ -174,19 +500,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end - context 'when the given table does not have a primary key' do - it 'raises an error' do - migration.execute(<<~SQL) - ALTER TABLE #{source_table} - DROP CONSTRAINT #{source_table}_pkey - SQL - - expect do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/primary key not defined for #{source_table}/) - end - end - context 'when an invalid partition column is given' do let(:invalid_column) { :_this_is_not_real } @@ -198,34 +511,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end describe 'constructing the partitioned table' do - it 'creates a table partitioned by the proper column' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - expect(connection.table_exists?(partitioned_table)).to be(true) - expect(connection.primary_key(partitioned_table)).to eq(new_primary_key) - - expect_table_partitioned_by(partitioned_table, [partition_column]) - end - - it 'requires the migration helper to be run in DDL mode' do - expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!) - - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - expect(connection.table_exists?(partitioned_table)).to be(true) - expect(connection.primary_key(partitioned_table)).to eq(new_primary_key) - - expect_table_partitioned_by(partitioned_table, [partition_column]) - end - - it 'changes the primary key datatype to bigint' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } - - expect(pk_column.sql_type).to eq('bigint') - end - context 'with a non-integer primary key datatype' do before do connection.create_table non_int_table, id: false do |t| @@ -248,23 +533,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end - it 'removes the default from the primary key column' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } - - expect(pk_column.default_function).to be_nil - end - - it 'creates the partitioned table with the same non-key columns' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key) - original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key) - - expect(copied_columns).to match_array(original_columns) - end - it 'creates a partition spanning over each month in the range given' do migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date @@ -350,75 +618,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end end - - describe 'keeping data in sync with the partitioned table' do - let(:partitioned_model) { Class.new(ActiveRecord::Base) } - let(:timestamp) { Time.utc(2019, 12, 1, 12).round } - - before do - partitioned_model.primary_key = :id - partitioned_model.table_name = partitioned_table - end - - it 'creates a trigger function on the original table' do - expect_function_not_to_exist(function_name) - expect_trigger_not_to_exist(source_table, trigger_name) - - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - expect_function_to_exist(function_name) - expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) - end - - it 'syncs inserts to the partitioned tables' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - expect(partitioned_model.count).to eq(0) - - first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) - second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - - expect(partitioned_model.count).to eq(2) - expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes) - expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) - end - - it 'syncs updates to the partitioned tables' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) - second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - - expect(partitioned_model.count).to eq(2) - - first_copy = partitioned_model.find(first_record.id) - second_copy = partitioned_model.find(second_record.id) - - expect(first_copy.attributes).to eq(first_record.attributes) - expect(second_copy.attributes).to eq(second_record.attributes) - - first_record.update!(age: 21, updated_at: timestamp + 1.hour) - - expect(partitioned_model.count).to eq(2) - expect(first_copy.reload.attributes).to eq(first_record.attributes) - expect(second_copy.reload.attributes).to eq(second_record.attributes) - end - - it 'syncs deletes to the partitioned tables' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - - first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) - second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - - expect(partitioned_model.count).to eq(2) - - first_record.destroy! - - expect(partitioned_model.count).to eq(1) - expect(partitioned_model.find_by_id(first_record.id)).to be_nil - expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) - end - end end describe '#drop_partitioned_table_for' do diff --git a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb index 44f47236ffa..91081cc88ea 100644 --- a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb +++ b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb @@ -55,6 +55,34 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar end end + shared_examples 'excluded path' do + it 'measures and logs the execution time' do + expect(::Gitlab::PathTraversal) + .not_to receive(:check_path_traversal!) + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ class_name: described_class.name, duration_ms: instance_of(Float) }) + .and_call_original + + expect(subject).to eq(fake_response) + end + + context 'with log_execution_time_path_traversal_middleware disabled' do + before do + stub_feature_flags(log_execution_time_path_traversal_middleware: false) + end + + it 'does nothing' do + expect(::Gitlab::PathTraversal) + .not_to receive(:check_path_traversal!) + expect(::Gitlab::AppLogger) + .not_to receive(:warn) + + expect(subject).to eq(fake_response) + end + end + end + shared_examples 'path traversal' do it 'logs the problem and measures the execution time' do expect(::Gitlab::PathTraversal) @@ -112,23 +140,90 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar let(:method) { 'get' } where(:path, :query_params, :shared_example_name) do - '/foo/bar' | {} | 'no issue' - '/foo/../bar' | {} | 'path traversal' - '/foo%2Fbar' | {} | 'no issue' - '/foo%2F..%2Fbar' | {} | 'path traversal' - '/foo%252F..%252Fbar' | {} | 'no issue' - '/foo/bar' | { x: 'foo' } | 'no issue' - '/foo/bar' | { x: 'foo/../bar' } | 'path traversal' - '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue' - '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue' - '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue' - '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal' + '/foo/bar' | {} | 'no issue' + '/foo/../bar' | {} | 'path traversal' + '/foo%2Fbar' | {} | 'no issue' + '/foo%2F..%2Fbar' | {} | 'path traversal' + '/foo%252F..%252Fbar' | {} | 'no issue' + + '/foo/bar' | { x: 'foo' } | 'no issue' + '/foo/bar' | { x: 'foo/../bar' } | 'path traversal' + '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue' + '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal' end with_them do it_behaves_like params[:shared_example_name] end + context 'for global search excluded paths' do + excluded_paths = %w[ + /search + /search/count + /api/v4/search + /api/v4/search.json + /api/v4/projects/4/search + /api/v4/projects/4/search.json + /api/v4/projects/4/-/search + /api/v4/projects/4/-/search.json + /api/v4/projects/my%2Fproject/search + /api/v4/projects/my%2Fproject/search.json + /api/v4/projects/my%2Fproject/-/search + /api/v4/projects/my%2Fproject/-/search.json + /api/v4/groups/4/search + /api/v4/groups/4/search.json + /api/v4/groups/4/-/search + /api/v4/groups/4/-/search.json + /api/v4/groups/my%2Fgroup/search + /api/v4/groups/my%2Fgroup/search.json + /api/v4/groups/my%2Fgroup/-/search + /api/v4/groups/my%2Fgroup/-/search.json + ] + query_params_with_no_path_traversal = [ + {}, + { x: 'foo' }, + { x: 'foo%2F..%2Fbar' }, + { x: 'foo%2F..%2Fbar' }, + { x: 'foo%252F..%252Fbar' } + ] + query_params_with_path_traversal = [ + { x: 'foo/../bar' } + ] + + excluded_paths.each do |excluded_path| + [query_params_with_no_path_traversal + query_params_with_path_traversal].flatten.each do |qp| + context "for excluded path #{excluded_path} with query params #{qp}" do + let(:query_params) { qp } + let(:path) { excluded_path } + + it_behaves_like 'excluded path' + end + end + + non_excluded_path = excluded_path.gsub('search', 'searchtest') + + query_params_with_no_path_traversal.each do |qp| + context "for non excluded path #{non_excluded_path} with query params #{qp}" do + let(:query_params) { qp } + let(:path) { non_excluded_path } + + it_behaves_like 'no issue' + end + end + + query_params_with_path_traversal.each do |qp| + context "for non excluded path #{non_excluded_path} with query params #{qp}" do + let(:query_params) { qp } + let(:path) { non_excluded_path } + + it_behaves_like 'path traversal' + end + end + end + end + context 'with a issues search path' do let(:query_params) { {} } let(:path) do @@ -149,6 +244,7 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar '/foo%2Fbar' | {} | 'no issue' '/foo%2F..%2Fbar' | {} | 'path traversal' '/foo%252F..%252Fbar' | {} | 'no issue' + '/foo/bar' | { x: 'foo' } | 'no issue' '/foo/bar' | { x: 'foo/../bar' } | 'no issue' '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue' @@ -160,6 +256,59 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar with_them do it_behaves_like params[:shared_example_name] end + + context 'for global search excluded paths' do + excluded_paths = %w[ + /search + /search/count + /api/v4/search + /api/v4/search.json + /api/v4/projects/4/search + /api/v4/projects/4/search.json + /api/v4/projects/4/-/search + /api/v4/projects/4/-/search.json + /api/v4/projects/my%2Fproject/search + /api/v4/projects/my%2Fproject/search.json + /api/v4/projects/my%2Fproject/-/search + /api/v4/projects/my%2Fproject/-/search.json + /api/v4/groups/4/search + /api/v4/groups/4/search.json + /api/v4/groups/4/-/search + /api/v4/groups/4/-/search.json + /api/v4/groups/my%2Fgroup/search + /api/v4/groups/my%2Fgroup/search.json + /api/v4/groups/my%2Fgroup/-/search + /api/v4/groups/my%2Fgroup/-/search.json + ] + all_query_params = [ + {}, + { x: 'foo' }, + { x: 'foo%2F..%2Fbar' }, + { x: 'foo%2F..%2Fbar' }, + { x: 'foo%252F..%252Fbar' }, + { x: 'foo/../bar' } + ] + + excluded_paths.each do |excluded_path| + all_query_params.each do |qp| + context "for excluded path #{excluded_path} with query params #{qp}" do + let(:query_params) { qp } + let(:path) { excluded_path } + + it_behaves_like 'excluded path' + end + + non_excluded_path = excluded_path.gsub('search', 'searchtest') + + context "for non excluded path #{non_excluded_path} with query params #{qp}" do + let(:query_params) { qp } + let(:path) { excluded_path.gsub('search', 'searchtest') } + + it_behaves_like 'no issue' + end + end + end + end end end @@ -179,6 +328,12 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar '/foo/bar' | { x: 'foo%2Fbar' } '/foo/bar' | { x: 'foo%2F..%2Fbar' } '/foo/bar' | { x: 'foo%252F..%252Fbar' } + '/search' | { x: 'foo/../bar' } + '/search' | { x: 'foo%2F..%2Fbar' } + '/search' | { x: 'foo%252F..%252Fbar' } + '%2Fsearch' | { x: 'foo/../bar' } + '%2Fsearch' | { x: 'foo%2F..%2Fbar' } + '%2Fsearch' | { x: 'foo%252F..%252Fbar' } end with_them do diff --git a/spec/rubocop/rubocop_spec.rb b/spec/rubocop/rubocop_spec.rb new file mode 100644 index 00000000000..a80a0f72bdf --- /dev/null +++ b/spec/rubocop/rubocop_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# No spec helper is `require`d because `fast_spec_helper` requires +# `active_support/all` and we want to ensure that `rubocop/rubocop` loads it. + +require 'rubocop' +require_relative '../../rubocop/rubocop' + +RSpec.describe 'rubocop/rubocop', feature_category: :tooling do + it 'loads activesupport to enhance Enumerable' do + expect(Enumerable.instance_methods).to include(:exclude?) + end +end diff --git a/spec/services/bulk_imports/batched_relation_export_service_spec.rb b/spec/services/bulk_imports/batched_relation_export_service_spec.rb index c361dfe5052..dd85961befd 100644 --- a/spec/services/bulk_imports/batched_relation_export_service_spec.rb +++ b/spec/services/bulk_imports/batched_relation_export_service_spec.rb @@ -71,29 +71,6 @@ RSpec.describe BulkImports::BatchedRelationExportService, feature_category: :imp expect(export.batches.count).to eq(0) end end - - context 'when exception occurs' do - it 'tracks exception and marks export as failed' do - allow_next_instance_of(BulkImports::Export) do |export| - allow(export).to receive(:update!).and_call_original - - allow(export) - .to receive(:update!) - .with(status_event: 'finish', total_objects_count: 0, batched: true, batches_count: 0, jid: jid, error: nil) - .and_raise(StandardError, 'Error!') - end - - expect(Gitlab::ErrorTracking) - .to receive(:track_exception) - .with(StandardError, portable_id: portable.id, portable_type: portable.class.name) - - service.execute - - export = portable.bulk_import_exports.first - - expect(export.reload.failed?).to eq(true) - end - end end describe '.cache_key' do diff --git a/spec/services/bulk_imports/relation_batch_export_service_spec.rb b/spec/services/bulk_imports/relation_batch_export_service_spec.rb index 7729c17098a..a18099a4189 100644 --- a/spec/services/bulk_imports/relation_batch_export_service_spec.rb +++ b/spec/services/bulk_imports/relation_batch_export_service_spec.rb @@ -34,10 +34,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor end it 'removes exported contents after export' do - double = instance_double(BulkImports::FileTransfer::ProjectConfig, export_path: 'foo') - - allow(BulkImports::FileTransfer).to receive(:config_for).and_return(double) - allow(double).to receive(:export_service_for).and_raise(StandardError, 'Error!') + allow(subject).to receive(:export_path).and_return('foo') allow(FileUtils).to receive(:remove_entry) expect(FileUtils).to receive(:remove_entry).with('foo') @@ -53,29 +50,10 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor allow(subject).to receive(:export_path).and_return('foo') allow(FileUtils).to receive(:remove_entry) - expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson') + expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson').and_call_original subject.execute end end - - context 'when exception occurs' do - before do - allow(service).to receive(:gzip).and_raise(StandardError, 'Error!') - end - - it 'marks batch as failed' do - expect(Gitlab::ErrorTracking) - .to receive(:track_exception) - .with(StandardError, portable_id: project.id, portable_type: 'Project') - - service.execute - batch.reload - - expect(batch.failed?).to eq(true) - expect(batch.objects_count).to eq(0) - expect(batch.error).to eq('Error!') - end - end end end diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb index bd8447e3d97..b7d6c424277 100644 --- a/spec/services/bulk_imports/relation_export_service_spec.rb +++ b/spec/services/bulk_imports/relation_export_service_spec.rb @@ -59,7 +59,7 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers let(:relation) { 'milestones' } it 'creates empty file on disk' do - expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson") + expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson").and_call_original subject.execute end @@ -118,39 +118,6 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers end end - context 'when exception occurs during export' do - shared_examples 'tracks exception' do |exception_class| - it 'tracks exception' do - expect(Gitlab::ErrorTracking) - .to receive(:track_exception) - .with(exception_class, portable_id: group.id, portable_type: group.class.name) - .and_call_original - - subject.execute - end - end - - before do - allow_next_instance_of(BulkImports::ExportUpload) do |upload| - allow(upload).to receive(:save!).and_raise(StandardError) - end - end - - it 'marks export as failed' do - subject.execute - - expect(export.reload.failed?).to eq(true) - end - - include_examples 'tracks exception', StandardError - - context 'when passed relation is not supported' do - let(:relation) { 'unsupported' } - - include_examples 'tracks exception', ActiveRecord::RecordInvalid - end - end - context 'when export was batched' do let(:relation) { 'milestones' } let(:export) { create(:bulk_import_export, group: group, relation: relation, batched: true, batches_count: 2) } diff --git a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb index d882cd8635a..f87952d1d0e 100644 --- a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb +++ b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb @@ -22,6 +22,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat end let(:expected_error_message) { error_parameter_missing } + let(:expected_custom_email_enabled) { false } let(:logger_params) { { category: 'custom_email_verification' } } before do @@ -30,7 +31,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat end shared_examples 'a failing verification process' do |expected_error_identifier| - it 'refuses to verify and sends result emails' do + it 'refuses to verify and sends result emails', :aggregate_failures do expect(Notify).to receive(:service_desk_verification_result_email).twice expect(Gitlab::AppLogger).to receive(:info).with(logger_params.merge( @@ -52,7 +53,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat end shared_examples 'an early exit from the verification process' do |expected_state| - it 'exits early' do + it 'exits early', :aggregate_failures do expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times expect(Gitlab::AppLogger).to receive(:warn).with(logger_params.merge( @@ -65,7 +66,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat verification.reset expect(response).to be_error - expect(settings).not_to be_custom_email_enabled + expect(settings.custom_email_enabled).to eq expected_custom_email_enabled expect(verification.state).to eq expected_state end end @@ -179,6 +180,26 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe' end + + context 'when already verified' do + let(:expected_error_message) { error_already_finished } + + before do + verification.mark_as_finished! + end + + it_behaves_like 'an early exit from the verification process', 'finished' + + context 'when enabled' do + let(:expected_custom_email_enabled) { true } + + before do + settings.update!(custom_email_enabled: true) + end + + it_behaves_like 'an early exit from the verification process', 'finished' + end + end end end end diff --git a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb index e27e2dc0ac8..8ee55d64a1b 100644 --- a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb +++ b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb @@ -23,4 +23,21 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import end end end + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => job_args } } + + it 'sets export status to failed and tracks the exception' do + portable = batch.export.portable + + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(kind_of(StandardError), portable_id: portable.id, portable_type: portable.class.name) + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300)) + + expect(batch.reload.failed?).to eq(true) + expect(batch.error.size).to eq(255) + end + end end diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb index 646af6c2a9c..55e2a238027 100644 --- a/spec/workers/bulk_imports/relation_export_worker_spec.rb +++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb @@ -63,4 +63,20 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d end end end + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => job_args } } + let!(:export) { create(:bulk_import_export, group: group, relation: relation) } + + it 'sets export status to failed and tracks the exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(kind_of(StandardError), portable_id: group.id, portable_type: group.class.name) + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300)) + + expect(export.reload.failed?).to eq(true) + expect(export.error.size).to eq(255) + end + end end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 37c878aa16b..ebe7dbcbce1 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -480,7 +480,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'X509CertificateRevokeWorker' => 3, 'ComplianceManagement::MergeRequests::ComplianceViolationsWorker' => 3, 'Zoekt::IndexerWorker' => 2, - 'Issuable::RelatedLinksCreateWorker' => 3 + 'Issuable::RelatedLinksCreateWorker' => 3, + 'BulkImports::RelationBatchExportWorker' => 3, + 'BulkImports::RelationExportWorker' => 3 }.merge(extra_retry_exceptions) end diff --git a/yarn.lock b/yarn.lock index ef4fd43d18a..36380d7144d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1274,10 +1274,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.69.0.tgz#bf76b8ffbe72a783807761a38abe8aaedcfe8c12" integrity sha512-Zu8Fcjhi3Bk26jZOptcD5F4SHWC7/KuAe00NULViCeswKdoda1k19B+9oCSbsbxY7vMoFuD20kiCJdBCpxb3HA== -"@gitlab/ui@67.5.1": - version "67.5.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-67.5.1.tgz#e331b34fa920f8f26ab0635dc84d15cbd4ac29e3" - integrity sha512-nsIlkZlU9Sig/KyVMkiHUp2Mcg/H2h4U5wN7ROnTv5heLhCP5oCvRpzGuazzhR/oemPqnKv/53ySAosDokhlmA== +"@gitlab/ui@67.5.2": + version "67.5.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-67.5.2.tgz#9a8d6008353b7250c1686a2a20fcae6630c97213" + integrity sha512-Qtkh9AEHjPrST/rl4SeY7Wh/ZB68GQO9jjM4Jll+3JicX3prg55uuFd4PATcq71DbXyIySZuDI/DrltgkhWByw== dependencies: "@floating-ui/dom" "1.2.9" bootstrap-vue "2.23.1" |