Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
commit585826cb22ecea5998a2c2a4675735c94bdeedac (patch)
tree5b05f0b30d33cef48963609e8a18a4dff260eab3 /lib
parentdf221d036e5d0c6c0ee4d55b9c97f481ee05dee8 (diff)
Add latest changes from gitlab-org/gitlab@16-6-stable-eev16.6.0-rc42
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb11
-rw-r--r--lib/api/bulk_imports.rb17
-rw-r--r--lib/api/ci/jobs.rb6
-rw-r--r--lib/api/ci/pipelines.rb29
-rw-r--r--lib/api/ci/runners.rb4
-rw-r--r--lib/api/commit_statuses.rb31
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb10
-rw-r--r--lib/api/entities/bulk_imports/entity_failure.rb10
-rw-r--r--lib/api/entities/commit_signature.rb17
-rw-r--r--lib/api/entities/group.rb3
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/responses/get.rb17
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/types/model_version.rb85
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb18
-rw-r--r--lib/api/entities/ml/mlflow/registered_model.rb18
-rw-r--r--lib/api/entities/wiki_page.rb4
-rw-r--r--lib/api/github/entities.rb219
-rw-r--r--lib/api/group_packages.rb6
-rw-r--r--lib/api/groups.rb1
-rw-r--r--lib/api/helm_packages.rb2
-rw-r--r--lib/api/helpers.rb20
-rw-r--r--lib/api/helpers/groups_helpers.rb3
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/helpers/kubernetes/agent_helpers.rb2
-rw-r--r--lib/api/helpers/packages/npm.rb10
-rw-r--r--lib/api/helpers/pagination_strategies.rb6
-rw-r--r--lib/api/helpers/rate_limiter.rb4
-rw-r--r--lib/api/internal/base.rb25
-rw-r--r--lib/api/internal/shellhorse.rb74
-rw-r--r--lib/api/invitations.rb36
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/maven_packages.rb4
-rw-r--r--lib/api/members.rb13
-rw-r--r--lib/api/merge_request_approvals.rb4
-rw-r--r--lib/api/ml/mlflow/api_helpers.rb8
-rw-r--r--lib/api/ml/mlflow/entrypoint.rb5
-rw-r--r--lib/api/ml/mlflow/experiments.rb5
-rw-r--r--lib/api/ml/mlflow/model_versions.rb32
-rw-r--r--lib/api/ml/mlflow/registered_models.rb96
-rw-r--r--lib/api/ml/mlflow/runs.rb5
-rw-r--r--lib/api/nuget_project_packages.rb2
-rw-r--r--lib/api/personal_access_tokens.rb8
-rw-r--r--lib/api/project_packages.rb4
-rw-r--r--lib/api/project_repository_storage_moves.rb10
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/pypi_packages.rb7
-rw-r--r--lib/api/releases.rb23
-rw-r--r--lib/api/resource_access_tokens.rb6
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/api/users.rb10
-rw-r--r--lib/api/v3/github.rb289
-rw-r--r--lib/api/vs_code/settings/entities/vs_code_setting_reference.rb23
-rw-r--r--lib/api/vs_code/settings/vs_code_settings_sync.rb87
-rw-r--r--lib/api/wikis.rb6
-rw-r--r--lib/atlassian/jira_connect/jira_user.rb6
-rw-r--r--lib/backup/gitaly_backup.rb2
-rw-r--r--lib/banzai/filter/asset_proxy_filter.rb3
-rw-r--r--lib/banzai/filter/math_filter.rb10
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb11
-rw-r--r--lib/banzai/reference_parser/user_parser.rb66
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb4
-rw-r--r--lib/bitbucket/representation/repo.rb4
-rw-r--r--lib/bulk_imports/clients/graphql.rb3
-rw-r--r--lib/bulk_imports/common/pipelines/entity_finisher.rb5
-rw-r--r--lib/bulk_imports/logger.rb11
-rw-r--r--lib/bulk_imports/ndjson_pipeline.rb2
-rw-r--r--lib/bulk_imports/pipeline/runner.rb40
-rw-r--r--lib/bulk_imports/pipeline_schema_info.rb36
-rw-r--r--lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb4
-rw-r--r--lib/bulk_imports/projects/pipelines/releases_pipeline.rb4
-rw-r--r--lib/bulk_imports/source_url_builder.rb57
-rw-r--r--lib/click_house/migration.rb89
-rw-r--r--lib/click_house/migration_support/migration_context.rb94
-rw-r--r--lib/click_house/migration_support/migration_error.rb54
-rw-r--r--lib/click_house/migration_support/migrator.rb160
-rw-r--r--lib/click_house/migration_support/schema_migration.rb71
-rw-r--r--lib/click_house/models/audit_event.rb55
-rw-r--r--lib/click_house/models/base_model.rb41
-rw-r--r--lib/container_registry/client.rb8
-rw-r--r--lib/container_registry/gitlab_api_client.rb20
-rw-r--r--lib/container_registry/tag.rb17
-rw-r--r--lib/generators/batched_background_migration/templates/queue_batched_background_migration.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template2
-rw-r--r--lib/generators/gitlab/snowplow_event_definition_generator.rb73
-rw-r--r--lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb5
-rw-r--r--lib/generators/gitlab/usage_metric_definition_generator.rb12
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/analytics/cycle_analytics/request_params.rb3
-rw-r--r--lib/gitlab/application_rate_limiter.rb1
-rw-r--r--lib/gitlab/auth.rb24
-rw-r--r--lib/gitlab/auth/saml/config.rb15
-rw-r--r--lib/gitlab/auth/two_factor_auth_verifier.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_packages_tags_project_id.rb30
-rw-r--r--lib/gitlab/background_migration/batched_migration_job.rb2
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb28
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb28
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb28
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb4
-rw-r--r--lib/gitlab/bitbucket_import/importers/issue_importer.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importers/issues_importer.rb18
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_request_importer.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb124
-rw-r--r--lib/gitlab/bitbucket_import/importers/repository_importer.rb11
-rw-r--r--lib/gitlab/bitbucket_import/loggable.rb4
-rw-r--r--lib/gitlab/checks/diff_check.rb3
-rw-r--r--lib/gitlab/checks/file_size_check/any_oversized_blobs.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb9
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb1
-rw-r--r--lib/gitlab/ci/build/context/build.rb10
-rw-r--r--lib/gitlab/ci/components/instance_path.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb23
-rw-r--r--lib/gitlab/ci/config/entry/pages.rb31
-rw-r--r--lib/gitlab/ci/config/entry/processable.rb2
-rw-r--r--lib/gitlab/ci/config/header/input.rb11
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/base_input.rb46
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb9
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/number_input.rb17
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/string_input.rb28
-rw-r--r--lib/gitlab/ci/jwt_v2.rb30
-rw-r--r--lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb7
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/base_source.rb46
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/container_scanning.rb41
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb27
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb2
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb16
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json1085
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json1017
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json975
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json1380
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json1083
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json970
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json994
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/composite.rb2
-rw-r--r--lib/gitlab/ci/status/waiting_for_callback.rb33
-rw-r--r--lib/gitlab/ci/templates/Cosign.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml23
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/yaml_processor/dag.rb6
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb6
-rw-r--r--lib/gitlab/composer/version_index.rb3
-rw-r--r--lib/gitlab/config/entry/validators.rb3
-rw-r--r--lib/gitlab/config_checker/puma_rugged_checker.rb28
-rw-r--r--lib/gitlab/database/dictionary.rb60
-rw-r--r--lib/gitlab/database/dynamic_model_helpers.rb3
-rw-r--r--lib/gitlab/database/gitlab_schema.rb27
-rw-r--r--lib/gitlab/database/migration.rb6
-rw-r--r--lib/gitlab/database/migration_helpers.rb6
-rw-r--r--lib/gitlab/database/migration_helpers/convert_to_bigint.rb88
-rw-r--r--lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb2
-rw-r--r--lib/gitlab/database/migrations/batched_background_migration_helpers.rb2
-rw-r--r--lib/gitlab/database/migrations/milestone_mixin.rb5
-rw-r--r--lib/gitlab/database/partitioning/partition_manager.rb19
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb117
-rw-r--r--lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb2
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb47
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb91
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb74
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb68
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb118
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb64
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb81
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb97
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb16
-rw-r--r--lib/gitlab/database/schema_cache_with_renamed_table.rb22
-rw-r--r--lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb55
-rw-r--r--lib/gitlab/database/tables_locker.rb2
-rw-r--r--lib/gitlab/database/tables_truncate.rb2
-rw-r--r--lib/gitlab/discussions_diff/file_collection.rb14
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb30
-rw-r--r--lib/gitlab/emoji.rb2
-rw-r--r--lib/gitlab/encrypted_command_base.rb14
-rw-r--r--lib/gitlab/encrypted_configuration.rb2
-rw-r--r--lib/gitlab/encrypted_ldap_command.rb2
-rw-r--r--lib/gitlab/encrypted_redis_command.rb56
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/blame.rb7
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/git/commit.rb3
-rw-r--r--lib/gitlab/git/ref.rb1
-rw-r--r--lib/gitlab/git/repository.rb1
-rw-r--r--lib/gitlab/git/rugged_impl/blob.rb107
-rw-r--r--lib/gitlab/git/rugged_impl/commit.rb115
-rw-r--r--lib/gitlab/git/rugged_impl/ref.rb20
-rw-r--r--lib/gitlab/git/rugged_impl/repository.rb79
-rw-r--r--lib/gitlab/git/rugged_impl/tree.rb147
-rw-r--r--lib/gitlab/git/rugged_impl/use_rugged.rb50
-rw-r--r--lib/gitlab/git/tree.rb5
-rw-r--r--lib/gitlab/git_access.rb4
-rw-r--r--lib/gitlab/git_access_project.rb2
-rw-r--r--lib/gitlab/git_audit_event.rb28
-rw-r--r--lib/gitlab/gitaly_client.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb9
-rw-r--r--lib/gitlab/gitaly_client/conflict_files_stitcher.rb10
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb7
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb8
-rw-r--r--lib/gitlab/github_import/attachments_downloader.rb24
-rw-r--r--lib/gitlab/github_import/client.rb10
-rw-r--r--lib/gitlab/github_import/issuable_finder.rb14
-rw-r--r--lib/gitlab/github_import/job_delay_calculator.rb2
-rw-r--r--lib/gitlab/github_import/label_finder.rb16
-rw-r--r--lib/gitlab/github_import/milestone_finder.rb18
-rw-r--r--lib/gitlab/github_import/object_counter.rb2
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb18
-rw-r--r--lib/gitlab/github_import/representation/to_hash.rb4
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/graphql/tracers/timer_tracer.rb4
-rw-r--r--lib/gitlab/group_search_results.rb2
-rw-r--r--lib/gitlab/identifier.rb9
-rw-r--r--lib/gitlab/import_export/command_line_util.rb21
-rw-r--r--lib/gitlab/import_export/error.rb4
-rw-r--r--lib/gitlab/import_export/project/sample/date_calculator.rb2
-rw-r--r--lib/gitlab/instrumentation/redis_base.rb6
-rw-r--r--lib/gitlab/instrumentation/redis_interceptor.rb4
-rw-r--r--lib/gitlab/instrumentation_helper.rb10
-rw-r--r--lib/gitlab/internal_events.rb2
-rw-r--r--lib/gitlab/issues/rebalancing/state.rb2
-rw-r--r--lib/gitlab/jira/http_client.rb15
-rw-r--r--lib/gitlab/jira/middleware.rb23
-rw-r--r--lib/gitlab/jira_import/base_importer.rb4
-rw-r--r--lib/gitlab/jira_import/issues_importer.rb2
-rw-r--r--lib/gitlab/job_waiter.rb29
-rw-r--r--lib/gitlab/kubernetes/kubeconfig/template.rb2
-rw-r--r--lib/gitlab/legacy_http.rb4
-rw-r--r--lib/gitlab/memory/reporter.rb8
-rw-r--r--lib/gitlab/memory/reports_uploader.rb4
-rw-r--r--lib/gitlab/merge_requests/mergeability/check_result.rb5
-rw-r--r--lib/gitlab/metrics/exporter/metrics_middleware.rb8
-rw-r--r--lib/gitlab/middleware/go.rb2
-rw-r--r--lib/gitlab/middleware/path_traversal_check.rb45
-rw-r--r--lib/gitlab/omniauth_initializer.rb11
-rw-r--r--lib/gitlab/optimistic_locking.rb4
-rw-r--r--lib/gitlab/pages/deployment_update.rb15
-rw-r--r--lib/gitlab/pagination/cursor_based_keyset.rb6
-rw-r--r--lib/gitlab/patch/sidekiq_cron_poller.rb2
-rw-r--r--lib/gitlab/patch/sidekiq_scheduled_enq.rb7
-rw-r--r--lib/gitlab/project_template.rb7
-rw-r--r--lib/gitlab/push_options.rb1
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb23
-rw-r--r--lib/gitlab/redis.rb1
-rw-r--r--lib/gitlab/redis/cluster_util.rb9
-rw-r--r--lib/gitlab/redis/multi_store.rb47
-rw-r--r--lib/gitlab/redis/pubsub.rb13
-rw-r--r--lib/gitlab/redis/shared_state.rb6
-rw-r--r--lib/gitlab/redis/wrapper.rb40
-rw-r--r--lib/gitlab/regex.rb1
-rw-r--r--lib/gitlab/regex/packages.rb8
-rw-r--r--lib/gitlab/regex/packages/protection/rules.rb15
-rw-r--r--lib/gitlab/request_forgery_protection.rb4
-rw-r--r--lib/gitlab/rugged_instrumentation.rb45
-rw-r--r--lib/gitlab/search_results.rb8
-rw-r--r--lib/gitlab/seeders/ci/catalog/resource_seeder.rb114
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/server_metrics.rb100
-rw-r--r--lib/gitlab/sidekiq_middleware/skip_jobs.rb13
-rw-r--r--lib/gitlab/sidekiq_status.rb13
-rw-r--r--lib/gitlab/tracking.rb3
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/usage/metric_definition.rb23
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb5
-rw-r--r--lib/gitlab/usage_data_counters/redis_counter.rb7
-rw-r--r--lib/gitlab/workhorse.rb3
-rw-r--r--lib/peek/views/rugged.rb46
-rw-r--r--lib/sbom/purl_type/converter.rb1
-rw-r--r--lib/sidebars/admin/menus/admin_settings_menu.rb10
-rw-r--r--lib/sidebars/explore/menus/catalog_menu.rb34
-rw-r--r--lib/sidebars/explore/panel.rb1
-rw-r--r--lib/sidebars/menu.rb1
-rw-r--r--lib/sidebars/organizations/menus/manage_menu.rb9
-rw-r--r--lib/sidebars/projects/menus/ci_cd_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/scope_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/settings_menu.rb4
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb1
-rw-r--r--lib/sidebars/user_settings/menus/comment_templates_menu.rb2
-rw-r--r--lib/tasks/gitlab/bulk_add_permission.rake2
-rw-r--r--lib/tasks/gitlab/click_house/migration.rake60
-rw-r--r--lib/tasks/gitlab/db.rake12
-rw-r--r--lib/tasks/gitlab/features.rake34
-rw-r--r--lib/tasks/gitlab/redis.rake23
-rw-r--r--lib/tasks/gitlab/seed/ci_catalog_resources.rake26
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake41
-rw-r--r--lib/tasks/tanuki_emoji.rake9
-rw-r--r--lib/unnested_in_filters/rewriter.rb3
-rw-r--r--lib/vs_code/settings.rb2
296 files changed, 11655 insertions, 2046 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 8a26ae7e6f6..43a21c11dbc 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -387,16 +387,7 @@ module API
mount ::API::Internal::MailRoom
mount ::API::Internal::ContainerRegistry::Migration
mount ::API::Internal::Workhorse
-
- version 'v3', using: :path do
- # Although the following endpoints are kept behind V3 namespace,
- # they're not deprecated neither should be removed when V3 get
- # removed. They're needed as a layer to integrate with Jira
- # Development Panel.
- namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do
- mount ::API::V3::Github
- end
- end
+ mount ::API::Internal::Shellhorse
route :any, '*path', feature_category: :not_owned do # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
error!('404 Not Found', 404)
diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb
index 9bcc16cf211..9dc0e5bae9b 100644
--- a/lib/api/bulk_imports.rb
+++ b/lib/api/bulk_imports.rb
@@ -214,6 +214,23 @@ module API
get ':import_id/entities/:entity_id' do
present bulk_import_entity, with: Entities::BulkImports::Entity
end
+
+ desc 'Get GitLab Migration entity failures' do
+ detail 'This feature was introduced in GitLab 16.6'
+ success code: 200, model: Entities::BulkImports::EntityFailure
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ end
+ params do
+ requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration"
+ requires :entity_id, type: Integer, desc: "The ID of GitLab Migration entity"
+ end
+ get ':import_id/entities/:entity_id/failures' do
+ present paginate(bulk_import_entity.failures), with: Entities::BulkImports::EntityFailure
+ end
end
end
end
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index 6f0a2ff7f62..250fe249489 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -57,7 +57,7 @@ module API
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project)
- present paginate_with_strategies(builds, paginator_params: { without_count: true }), with: Entities::Ci::Job
+ present paginate_with_strategies(builds, user_project, paginator_params: { without_count: true }), with: Entities::Ci::Job
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -122,10 +122,10 @@ module API
requires :job_id, type: Integer, desc: 'The ID of a job', documentation: { example: 88 }
end
post ':id/jobs/:job_id/cancel', urgency: :low, feature_category: :continuous_integration do
- authorize_update_builds!
+ authorize_cancel_builds!
build = find_build!(params[:job_id])
- authorize!(:update_build, build)
+ authorize!(:cancel_build, build)
build.cancel
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index bd5c04f401b..b5123ab49dc 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -288,6 +288,33 @@ module API
end
end
+ desc 'Updates pipeline metadata' do
+ detail 'This feature was introduced in GitLab 16.6'
+ success status: 200, model: Entities::Ci::PipelineWithMetadata
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID', documentation: { example: 18 }
+ requires :name, type: String, desc: 'The name of the pipeline', documentation: { example: 'Deployment to production' }
+ end
+ route_setting :authentication, job_token_allowed: true
+ put ':id/pipelines/:pipeline_id/metadata', urgency: :low, feature_category: :continuous_integration do
+ authorize! :update_pipeline, pipeline
+
+ response = ::Ci::Pipelines::UpdateMetadataService.new(pipeline, params.slice(:name)).execute
+
+ if response.success?
+ present response.payload, with: Entities::Ci::PipelineWithMetadata
+ else
+ render_api_error_with_reason!(response.reason, response.message, response.payload.join(', '))
+ end
+ end
+
desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success status: 201, model: Entities::Ci::Pipeline
@@ -325,7 +352,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID', documentation: { example: 18 }
end
post ':id/pipelines/:pipeline_id/cancel', urgency: :low, feature_category: :continuous_integration do
- authorize! :update_pipeline, pipeline
+ authorize! :cancel_pipeline, pipeline
# TODO: inconsistent behavior: when pipeline is not cancelable we should return an error
::Ci::CancelPipelineService.new(pipeline: pipeline, current_user: current_user).execute
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index 42817c782f4..17bee275c51 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -24,6 +24,9 @@ module API
desc: 'The status of runners to return'
optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
desc: 'A list of runner tags', documentation: { example: "['macos', 'shell']" }
+ optional :version_prefix, type: String, desc: 'The version prefix of runners to return', documentation: { example: "'15.1.' or '16.'" },
+ regexp: /^[\d+.]+/
+
use :pagination
end
@@ -46,6 +49,7 @@ module API
runners = filter_runners(runners, params[:type], allowed_scopes: ::Ci::Runner::AVAILABLE_TYPES)
runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
runners = filter_runners(runners, params[:paused] ? 'paused' : 'active', allowed_scopes: %w[paused active]) if params.include?(:paused)
+ runners = runners.with_version_prefix(params[:version_prefix]) if params[:version_prefix]
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
runners
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index acb64cd0d3a..62b2885f955 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -94,37 +94,6 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
helpers do
- def commit
- strong_memoize(:commit) do
- user_project.commit(params[:sha])
- end
- end
-
- def all_matching_pipelines
- pipelines = user_project.ci_pipelines.newest_first(sha: commit.sha)
- pipelines = pipelines.for_ref(params[:ref]) if params[:ref]
- pipelines = pipelines.id_in(params[:pipeline_id]) if params[:pipeline_id]
- pipelines
- end
-
- def apply_job_state!(job)
- case params[:state]
- when 'pending'
- job.enqueue!
- when 'running'
- job.enqueue
- job.run!
- when 'success'
- job.success!
- when 'failed'
- job.drop!(:api_failure)
- when 'canceled'
- job.cancel!
- else
- render_api_error!('invalid state', 400)
- end
- end
-
def optional_commit_status_params
updatable_optional_attributes = %w[target_url description coverage]
attributes_for_keys(updatable_optional_attributes)
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index bfaba5c4d7a..19d63a39242 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -202,7 +202,8 @@ module API
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
available_packages =
- if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if endpoint_scope != :project &&
+ Feature.enabled?(:npm_allow_packages_in_multiple_projects, group_or_namespace)
finder_for_endpoint_scope(package_name).execute
else
::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
@@ -218,9 +219,8 @@ module API
target: project_or_nil,
package_name: package_name
) do
- if endpoint_scope == :project || Feature.disabled?(:npm_allow_packages_in_multiple_projects)
- authorize_read_package!(project)
- elsif Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if endpoint_scope != :project &&
+ Feature.enabled?(:npm_allow_packages_in_multiple_projects, group_or_namespace)
available_packages_to_user = ::Packages::Npm::PackagesForUserFinder.new(
current_user,
group_or_namespace,
@@ -232,6 +232,8 @@ module API
end
available_packages = available_packages_to_user
+ else
+ authorize_read_package!(project)
end
not_found!('Packages') if available_packages.empty?
diff --git a/lib/api/entities/bulk_imports/entity_failure.rb b/lib/api/entities/bulk_imports/entity_failure.rb
index 3e69e7fa2aa..08708a7c961 100644
--- a/lib/api/entities/bulk_imports/entity_failure.rb
+++ b/lib/api/entities/bulk_imports/entity_failure.rb
@@ -4,18 +4,14 @@ module API
module Entities
module BulkImports
class EntityFailure < Grape::Entity
- expose :relation, documentation: { type: 'string', example: 'group' }
- expose :pipeline_step, as: :step, documentation: { type: 'string', example: 'extractor' }
+ expose :relation, documentation: { type: 'string', example: 'label' }
expose :exception_message, documentation: { type: 'string', example: 'error message' } do |failure|
::Projects::ImportErrorFilter.filter_message(failure.exception_message.truncate(72))
end
expose :exception_class, documentation: { type: 'string', example: 'Exception' }
expose :correlation_id_value, documentation: { type: 'string', example: 'dfcf583058ed4508e4c7c617bd7f0edd' }
- expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' }
- expose :pipeline_class, documentation: {
- type: 'string', example: 'BulkImports::Groups::Pipelines::GroupPipeline'
- }
- expose :pipeline_step, documentation: { type: 'string', example: 'extractor' }
+ expose :source_url, documentation: { type: 'string', example: 'https://source.gitlab.com/group/-/epics/1' }
+ expose :source_title, documentation: { type: 'string', example: 'title' }
end
end
end
diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb
index 9c30c3c59ea..cdd63df77f0 100644
--- a/lib/api/entities/commit_signature.rb
+++ b/lib/api/entities/commit_signature.rb
@@ -6,27 +6,24 @@ module API
expose :signature_type, documentation: { type: 'string', example: 'PGP' }
expose :signature, merge: true do |commit, options|
- if commit.signature.is_a?(::CommitSignatures::GpgSignature) || commit.raw_commit_from_rugged?
+ case commit.signature
+ when ::CommitSignatures::GpgSignature
::API::Entities::GpgCommitSignature.represent commit_signature(commit), options
- elsif commit.signature.is_a?(::CommitSignatures::X509CommitSignature)
+ when ::CommitSignatures::X509CommitSignature
::API::Entities::X509Signature.represent commit.signature, options
- elsif commit.signature.is_a?(::CommitSignatures::SshSignature)
+ when ::CommitSignatures::SshSignature
::API::Entities::SshSignature.represent(commit.signature, options)
end
end
- expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |commit, _|
- commit.raw_commit_from_rugged? ? "rugged" : "gitaly"
+ expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |_commit, _|
+ "gitaly"
end
private
def commit_signature(commit)
- if commit.raw_commit_from_rugged?
- commit.gpg_commit.signature
- else
- commit.signature
- end
+ commit.signature
end
end
end
diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb
index d18a29ce4d4..1a1765c2e0a 100644
--- a/lib/api/entities/group.rb
+++ b/lib/api/entities/group.rb
@@ -10,7 +10,8 @@ module API
expose :project_creation_level_str, as: :project_creation_level
expose :auto_devops_enabled
expose :subgroup_creation_level_str, as: :subgroup_creation_level
- expose :emails_disabled
+ expose(:emails_disabled, documentation: { type: 'boolean' }) { |group, options| group.emails_disabled? }
+ expose :emails_enabled, documentation: { type: 'boolean' }
expose :mentions_disabled
expose :lfs_enabled?, as: :lfs_enabled
expose :default_branch_protection
diff --git a/lib/api/entities/ml/mlflow/model_versions/responses/get.rb b/lib/api/entities/ml/mlflow/model_versions/responses/get.rb
new file mode 100644
index 00000000000..14baae03644
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/responses/get.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Responses
+ class Get < Grape::Entity
+ expose :model_version, with: Types::ModelVersion
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb b/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb
new file mode 100644
index 00000000000..407158521f7
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Types
+ class ModelVersion < Grape::Entity
+ expose :name
+ expose :version
+ expose :creation_timestamp, documentation: { type: Integer }
+ expose :last_updated_timestamp, documentation: { type: Integer }
+ expose :user_id
+ expose :current_stage
+ expose :description
+ expose :source
+ expose :run_id
+ expose :status
+ expose :status_message
+ expose :metadata
+ expose :run_link
+ expose :aliases, documentation: { is_array: true, type: String }
+
+ private
+
+ def name
+ object.model.name
+ end
+
+ def creation_timestamp
+ object.created_at.to_i
+ end
+
+ def last_updated_timestamp
+ object.updated_at.to_i
+ end
+
+ def user_id
+ nil
+ end
+
+ def current_stage
+ "development"
+ end
+
+ def description
+ ""
+ end
+
+ def source
+ model_name = object.model.name
+ "api/v4/projects/(id)/packages/ml_models/#{model_name}/model_version/"
+ end
+
+ def run_id
+ ""
+ end
+
+ def status
+ "READY"
+ end
+
+ def status_message
+ ""
+ end
+
+ def metadata
+ []
+ end
+
+ def run_link
+ ""
+ end
+
+ def aliases
+ []
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb b/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb
new file mode 100644
index 00000000000..f5ad3bf3fb9
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Types
+ class ModelVersionTag < Grape::Entity
+ expose :key
+ expose :value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/registered_model.rb b/lib/api/entities/ml/mlflow/registered_model.rb
new file mode 100644
index 00000000000..1ff983e1611
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/registered_model.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class RegisteredModel < Grape::Entity
+ expose :name
+ expose :created_at, as: :creation_timestamp
+ expose :updated_at, as: :last_updated_timestamp
+ expose :description
+ expose(:user_id) { |model| model.user_id.to_s }
+ expose :metadata, as: :tags, using: KeyValue
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb
index 0f3fdd586a3..8b2c951ecf2 100644
--- a/lib/api/entities/wiki_page.rb
+++ b/lib/api/entities/wiki_page.rb
@@ -22,6 +22,10 @@ module API
expose :encoding, documentation: { type: 'string', example: 'UTF-8' } do |wiki_page|
wiki_page.content.encoding.name
end
+
+ expose :front_matter, documentation: { type: 'Hash', example: { title: "deploy" } }, if: ->(wiki_page) {
+ ::Feature.enabled?(:wiki_front_matter_title, wiki_page.container)
+ }
end
end
end
diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb
deleted file mode 100644
index 125985f0e23..00000000000
--- a/lib/api/github/entities.rb
+++ /dev/null
@@ -1,219 +0,0 @@
-# frozen_string_literal: true
-
-# Simplified version of Github API entities.
-# It's mainly used to mimic Github API and integrate with Jira Development Panel.
-#
-module API
- module Github
- module Entities
- class Repository < Grape::Entity
- expose :id
- expose :owner do |project, options|
- root_namespace = options[:root_namespace] || project.root_namespace
-
- { login: root_namespace.path }
- end
- expose :name do |project, options|
- ::Gitlab::Jira::Dvcs.encode_project_name(project)
- end
- end
-
- class BranchCommit < Grape::Entity
- expose :id, as: :sha
- expose :type do |_|
- 'commit'
- end
- end
-
- class RepoCommit < Grape::Entity
- expose :id, as: :sha
- expose :author do |commit|
- {
- login: commit.author&.username,
- email: commit.author_email
- }
- end
- expose :committer do |commit|
- {
- login: commit.author&.username,
- email: commit.committer_email
- }
- end
- expose :commit do |commit|
- {
- author: {
- name: commit.author_name,
- email: commit.author_email,
- date: commit.authored_date.iso8601,
- type: 'User'
- },
- committer: {
- name: commit.committer_name,
- email: commit.committer_email,
- date: commit.committed_date.iso8601,
- type: 'User'
- },
- message: commit.safe_message
- }
- end
- expose :parents do |commit|
- commit.parent_ids.map { |id| { sha: id } }
- end
- expose :files do |_commit, options|
- options[:diff_files].flat_map do |diff|
- additions = diff.added_lines
- deletions = diff.removed_lines
-
- if diff.new_file?
- {
- status: 'added',
- filename: diff.new_path,
- additions: additions,
- changes: additions
- }
- elsif diff.deleted_file?
- {
- status: 'removed',
- filename: diff.old_path,
- deletions: deletions,
- changes: deletions
- }
- elsif diff.renamed_file?
- [
- {
- status: 'removed',
- filename: diff.old_path,
- deletions: deletions,
- changes: deletions
- },
- {
- status: 'added',
- filename: diff.new_path,
- additions: additions,
- changes: additions
- }
- ]
- else
- {
- status: 'modified',
- filename: diff.new_path,
- additions: additions,
- deletions: deletions,
- changes: (additions + deletions)
- }
- end
- end
- end
- end
-
- class Branch < Grape::Entity
- expose :name
-
- expose :commit, using: BranchCommit do |repo_branch, options|
- options[:project].repository.commit(repo_branch.dereferenced_target)
- end
- end
-
- class User < Grape::Entity
- expose :id
- expose :username, as: :login
- expose :user_url, as: :url
- expose :user_url, as: :html_url
- expose :avatar_url do |user|
- user.avatar_url(only_path: false)
- end
-
- private
-
- def user_url
- Gitlab::Routing.url_helpers.user_url(object)
- end
- end
-
- class NoteableComment < Grape::Entity
- expose :id
- expose :author, as: :user, using: User
- expose :note, as: :body
- expose :created_at
- end
-
- class PullRequest < Grape::Entity
- expose :title
- expose :assignee, using: User do |merge_request|
- merge_request.assignee
- end
- expose :author, as: :user, using: User
- expose :created_at
- expose :description, as: :body
- # Since Jira service requests `/repos/-/jira/pulls` (without project
- # scope), we need to make it work with ID instead IID.
- expose :id, as: :number
- # GitHub doesn't have a "merged" or "closed" state. It's just "open" or
- # "closed".
- expose :state do |merge_request|
- case merge_request.state
- when 'opened', 'locked'
- 'open'
- when 'merged'
- 'closed'
- else
- merge_request.state
- end
- end
- expose :merged?, as: :merged
- expose :merged_at do |merge_request|
- merge_request.metrics&.merged_at
- end
- expose :closed_at do |merge_request|
- merge_request.metrics&.latest_closed_at
- end
- expose :updated_at
- expose :html_url do |merge_request|
- Gitlab::UrlBuilder.build(merge_request)
- end
- expose :head do
- expose :source_branch, as: :label
- expose :source_branch, as: :ref
- expose :source_project, as: :repo, using: Repository
- end
- expose :base do
- expose :target_branch, as: :label
- expose :target_branch, as: :ref
- expose :target_project, as: :repo, using: Repository
- end
- end
-
- class PullRequestPayload < Grape::Entity
- expose :action do |merge_request|
- case merge_request.state
- when 'merged', 'closed'
- 'closed'
- else
- 'opened'
- end
- end
-
- expose :id
- expose :pull_request, using: PullRequest do |merge_request|
- merge_request
- end
- end
-
- class PullRequestEvent < Grape::Entity
- expose :id do |merge_request|
- updated_at = merge_request.updated_at.to_i
- "#{merge_request.id}-#{updated_at}"
- end
- expose :type do |_merge_request|
- 'PullRequestEvent'
- end
- expose :updated_at, as: :created_at
- expose :payload, using: PullRequestPayload do |merge_request|
- # The merge request data is used by PullRequestPayload and PullRequest, so we just provide it
- # here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request.
- merge_request
- end
- end
- end
- end
-end
diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb
index c2b4cbf732f..b363f59b7ad 100644
--- a/lib/api/group_packages.rb
+++ b/lib/api/group_packages.rb
@@ -47,6 +47,9 @@ module API
optional :package_name,
type: String,
desc: 'Return packages with this name'
+ optional :package_version,
+ type: String,
+ desc: 'Return packages with this version'
optional :include_versionless,
type: Boolean,
desc: 'Returns packages without a version'
@@ -60,7 +63,8 @@ module API
current_user,
user_group,
declared(params).slice(
- :exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status
+ :exclude_subgroups, :order_by, :sort, :package_type, :package_name,
+ :package_version, :include_versionless, :status
)
).execute
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 2efdfe109f7..1ff64cd2ffd 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -254,6 +254,7 @@ module API
group = find_group!(params[:id])
group.preload_shared_group_links
+ mark_throttle! :update_namespace_name, scope: group if params.key?(:name) && params[:name].present?
authorize! :admin_group, group
group.remove_avatar! if params.key?(:avatar) && params[:avatar].nil?
diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb
index 8260d8a88f8..c811f47cb5b 100644
--- a/lib/api/helm_packages.rb
+++ b/lib/api/helm_packages.rb
@@ -75,6 +75,8 @@ module API
requires :file_name, type: String, desc: 'Helm package file name', documentation: { example: 'mychart' }
end
get ":channel/charts/:file_name.tgz" do
+ not_found!("Format #{params[:format]}") unless params[:format].nil?
+
project = authorized_user_project(action: :read_package)
authorize_read_package!(project)
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 56b157f662a..bb94d5d14d0 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -141,7 +141,7 @@ module API
def find_project(id)
return unless id
- projects = Project.without_deleted.not_hidden
+ projects = find_project_scopes
if id.is_a?(Integer) || id =~ INTEGER_ID_REGEX
projects.find_by(id: id)
@@ -151,6 +151,11 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ # Can be overriden by API endpoints
+ def find_project_scopes
+ Project.without_deleted.not_hidden
+ end
+
def find_project!(id)
project = find_project(id)
@@ -337,6 +342,12 @@ module API
unauthorized!
end
+ def authenticate_by_gitlab_shell_or_workhorse_token!
+ return require_gitlab_workhorse! unless headers[GITLAB_SHELL_API_HEADER].present?
+
+ authenticate_by_gitlab_shell_token!
+ end
+
def authenticated_with_can_read_all_resources!
authenticate!
forbidden! unless current_user.can_read_all_resources?
@@ -391,6 +402,10 @@ module API
authorize! :update_build, user_project
end
+ def authorize_cancel_builds!
+ authorize! :cancel_build, user_project
+ end
+
def require_repository_enabled!(subject = :global)
not_found!("Repository") unless user_project.feature_available?(:repository, current_user)
end
@@ -758,6 +773,7 @@ module API
finder_params[:id_before] = sanitize_id_param(params[:id_before]) if params[:id_before]
finder_params[:updated_after] = declared_params[:updated_after] if declared_params[:updated_after]
finder_params[:updated_before] = declared_params[:updated_before] if declared_params[:updated_before]
+ finder_params[:include_pending_delete] = declared_params[:include_pending_delete] if declared_params[:include_pending_delete]
finder_params
end
@@ -891,7 +907,7 @@ module API
def project_moved?(id, project)
return false unless Feature.enabled?(:api_redirect_moved_projects)
return false unless id.is_a?(String) && id.include?('/')
- return false if project.blank? || id == project.full_path
+ return false if project.blank? || project.full_path.casecmp?(id)
return false unless params[:id] == id
true
diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb
index f7802938d8b..fbe13bfe8f7 100644
--- a/lib/api/helpers/groups_helpers.rb
+++ b/lib/api/helpers/groups_helpers.rb
@@ -18,7 +18,8 @@ module API
optional :project_creation_level, type: String, values: ::Gitlab::Access.project_creation_string_values, desc: 'Determine if developers can create projects in the group', as: :project_creation_level_str
optional :auto_devops_enabled, type: Boolean, desc: 'Default to Auto DevOps pipeline for all projects within this group'
optional :subgroup_creation_level, type: String, values: ::Gitlab::Access.subgroup_creation_string_values, desc: 'Allowed to create subgroups', as: :subgroup_creation_level_str
- optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
+ optional :emails_disabled, type: Boolean, desc: '_(Deprecated)_ Disable email notifications. Use: emails_enabled'
+ optional :emails_enabled, type: Boolean, desc: 'Enable email notifications'
optional :mentions_disabled, type: Boolean, desc: 'Disable a group from getting mentioned'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index f66f899c98b..0c5b12d48e9 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -123,6 +123,10 @@ module API
# Defined in EE
end
+ def need_git_audit_event?
+ false
+ end
+
private
def repository_path
diff --git a/lib/api/helpers/kubernetes/agent_helpers.rb b/lib/api/helpers/kubernetes/agent_helpers.rb
index 50a8c2a5aed..aa4f4310e1d 100644
--- a/lib/api/helpers/kubernetes/agent_helpers.rb
+++ b/lib/api/helpers/kubernetes/agent_helpers.rb
@@ -41,7 +41,7 @@ module API
end
def agent_has_access_to_project?(project)
- Guest.can?(:download_code, project) || agent.has_access_to?(project)
+ ::Users::Anonymous.can?(:download_code, project) || agent.has_access_to?(project)
end
def increment_unique_events
diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb
index ef3da055b19..c91eef0c4b0 100644
--- a/lib/api/helpers/packages/npm.rb
+++ b/lib/api/helpers/packages/npm.rb
@@ -64,7 +64,7 @@ module API
package_name = params[:package_name]
namespace =
- if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if Feature.enabled?(:npm_allow_packages_in_multiple_projects, top_namespace_from(package_name))
top_namespace_from(package_name)
else
namespace_path = ::Packages::Npm.scope_of(package_name)
@@ -94,10 +94,12 @@ module API
private
def top_namespace_from(package_name)
- namespace_path = ::Packages::Npm.scope_of(package_name)
- return unless namespace_path
+ strong_memoize_with(:top_namespace_from, package_name) do
+ namespace_path = ::Packages::Npm.scope_of(package_name)
+ next unless namespace_path
- Namespace.top_most.by_path(namespace_path)
+ Namespace.top_most.by_path(namespace_path)
+ end
end
def group
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 5fbc3081ee8..4353ba0e99a 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -50,7 +50,7 @@ module API
offset_limit = limit_for_scope(request_scope)
if (Gitlab::Pagination::Keyset.available_for_type?(relation) ||
cursor_based_keyset_pagination_supported?(relation)) &&
- cursor_based_keyset_pagination_enforced?(relation) &&
+ cursor_based_keyset_pagination_enforced?(request_scope, relation) &&
offset_limit_exceeded?(offset_limit)
return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
@@ -65,8 +65,8 @@ module API
Gitlab::Pagination::CursorBasedKeyset.available_for_type?(relation)
end
- def cursor_based_keyset_pagination_enforced?(relation)
- Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(relation)
+ def cursor_based_keyset_pagination_enforced?(request_scope, relation)
+ Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(request_scope, relation)
end
def keyset_pagination_enabled?
diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb
index be92277c25a..39940d86fbf 100644
--- a/lib/api/helpers/rate_limiter.rb
+++ b/lib/api/helpers/rate_limiter.rb
@@ -18,6 +18,10 @@ module API
render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
end
+
+ def mark_throttle!(key, scope:)
+ Gitlab::ApplicationRateLimiter.throttled?(key, scope: scope)
+ end
end
end
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index f9dc888fbeb..87b3838fb85 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -4,12 +4,18 @@ module API
# Internal access API
module Internal
class Base < ::API::Base
+ include Gitlab::RackLoadBalancingHelpers
+
before { authenticate_by_gitlab_shell_token! }
before do
api_endpoint = env['api.endpoint']
feature_category = api_endpoint.options[:for].try(:feature_category_for_app, api_endpoint).to_s
+ if actor.user
+ load_balancer_stick_request(::User, :user, actor.user.id)
+ end
+
Gitlab::ApplicationContext.push(
user: -> { actor&.user },
project: -> { project },
@@ -49,6 +55,11 @@ module API
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if container
+ # Snapshot repositories have different relative path than the main repository. For access
+ # checks that need quarantined objects the relative path in also sent with Gitaly RPCs
+ # calls as a header.
+ populate_relative_path(params[:relative_path])
+
actor.update_last_used_at!
check_result = access_check_result
@@ -66,7 +77,8 @@ module API
git_config_options: ["uploadpack.allowFilter=true",
"uploadpack.allowAnySHA1InWant=true"],
gitaly: gitaly_payload(params[:action]),
- gl_console_messages: check_result.console_messages
+ gl_console_messages: check_result.console_messages,
+ need_audit: need_git_audit_event?
}.merge!(actor.key_details)
# Custom option for git-receive-pack command
@@ -77,7 +89,9 @@ module API
payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
end
- send_git_audit_streaming_event(protocol: params[:protocol], action: params[:action])
+ unless Feature.enabled?(:log_git_streaming_audit_events, project)
+ send_git_audit_streaming_event(protocol: params[:protocol], action: params[:action])
+ end
response_with_status(**payload)
when ::Gitlab::GitAccessResult::CustomAction
@@ -88,6 +102,12 @@ module API
end
# rubocop: enable Metrics/AbcSize
+ def populate_relative_path(relative_path)
+ return unless Gitlab::SafeRequestStore.active?
+
+ Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
+ end
+
def validate_actor(actor)
return 'Could not find the given key' unless actor.key
@@ -112,6 +132,7 @@ module API
# username - user name for Git over SSH in keyless SSH cert mode
# protocol - Git access protocol being used, e.g. HTTP or SSH
# project - project full_path (not path on disk)
+ # relative_path - relative path of repository having access checks performed.
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
# check_ip - optional, only in EE version, may limit access to
diff --git a/lib/api/internal/shellhorse.rb b/lib/api/internal/shellhorse.rb
new file mode 100644
index 00000000000..89210c8a78a
--- /dev/null
+++ b/lib/api/internal/shellhorse.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module API
+ module Internal
+ class Shellhorse < ::API::Base
+ before { authenticate_by_gitlab_shell_or_workhorse_token! }
+
+ helpers ::API::Helpers::InternalHelpers
+
+ COMMANDS_TO_AUDIT = %w[git-upload-pack git-receive-pack].freeze
+
+ helpers do
+ def check_clone_or_pull_or_push_verb(params)
+ return 'push' if params[:action] == 'git-receive-pack'
+
+ # we must set the default value for wants/haves because
+ # gitlab shell/workhorse will trim the whole posted params
+ # json key if its value is 0
+ wants = haves = 0
+ if params.key?(:packfile_stats)
+ wants = Integer(params[:packfile_stats][:wants]) if params[:packfile_stats][:wants].present?
+ haves = Integer(params[:packfile_stats][:haves]) if params[:packfile_stats][:haves].present?
+ end
+
+ wants > 0 && haves == 0 ? 'clone' : 'pull'
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'shellhorse' do
+ params do
+ requires :action, type: String
+ requires :protocol, type: String
+ requires :gl_repository, type: String # repository identifier, such as project-7
+ optional :packfile_stats, type: Hash do
+ # wants is the number of objects the client announced it wants.
+ optional :wants, type: Integer
+ # haves is the number of objects the client announced it has.
+ optional :haves, type: Integer
+ end
+ end
+
+ post '/git_audit_event', feature_category: :source_code_management do
+ unless COMMANDS_TO_AUDIT.include?(params[:action])
+ break response_with_status(code: 400, success: false, message: "No valid action specified")
+ end
+
+ check_result = access_check_result
+ break check_result if unsuccessful_response?(check_result)
+
+ unless need_git_audit_event?
+ break response_with_status(code: 200, success: false, message: "No git audit event needed")
+ end
+
+ unless check_result.is_a?(::Gitlab::GitAccessResult::Success)
+ break response_with_status(code: 500, success: false,
+ message: ::API::Helpers::InternalHelpers::UNKNOWN_CHECK_RESULT_ERROR)
+ end
+
+ msg = {
+ protocol: params[:protocol],
+ action: params[:action],
+ verb: check_clone_or_pull_or_push_verb(params)
+ }
+ send_git_audit_streaming_event(msg)
+ response_with_status(message: msg)
+ end
+ end
+ end
+ end
+ end
+end
+
+API::Internal::Shellhorse.prepend_mod_with('API::Internal::Shellhorse')
diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb
index 34f9538b047..d625b2c0fe6 100644
--- a/lib/api/invitations.rb
+++ b/lib/api/invitations.rb
@@ -10,6 +10,12 @@ module API
helpers ::API::Helpers::MembersHelpers
+ helpers do
+ params :invitation_params_ee do
+ # Overriden in EE
+ end
+ end
+
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
@@ -26,6 +32,8 @@ module API
optional :user_id, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The user ID of the new member or multiple IDs separated by commas.'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
+
+ use :invitation_params_ee
end
post ":id/invitations", urgency: :low do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/354016')
@@ -34,11 +42,7 @@ module API
source = find_source(source_type, params[:id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
create_service_params = params.merge(source: source)
@@ -61,11 +65,7 @@ module API
source = find_source(source_type, params[:id])
query = params[:query]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invitations = paginate(retrieve_member_invitations(source, query))
@@ -80,16 +80,14 @@ module API
requires :email, type: String, desc: 'The email address of the invitation'
optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
+
+ use :invitation_params_ee
end
put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
source = find_source(source_type, params.delete(:id))
invite_email = params[:email]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invite = retrieve_member_invitations(source, invite_email).first
not_found! unless invite
@@ -127,11 +125,7 @@ module API
source = find_source(source_type, params[:id])
invite_email = params[:email]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invite = retrieve_member_invitations(source, invite_email).first
not_found! unless invite
@@ -145,3 +139,5 @@ module API
end
end
end
+
+API::Members.prepend_mod
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index 26619e6924f..b2f0f54e380 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -29,7 +29,7 @@ module API
not_found! 'Commit' unless user_project.commit(sha).present?
- content = user_project.repository.gitlab_ci_yml_for(sha, user_project.ci_config_path_or_default)
+ content = user_project.repository.blob_data_at(sha, user_project.ci_config_path_or_default)
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user, sha: sha)
.validate(content, dry_run: params[:dry_run], ref: params[:ref] || user_project.default_branch)
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
index 517de98a148..14c3fccee32 100644
--- a/lib/api/maven_packages.rb
+++ b/lib/api/maven_packages.rb
@@ -228,7 +228,7 @@ module API
requires :path, type: String, desc: 'Package path', documentation: { example: 'foo/bar/mypkg/1.0-SNAPSHOT' }
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex, documentation: { example: 'mypkg-1.0-SNAPSHOT.pom' }
end
- route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true, basic_auth_personal_access_token: true
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload!
@@ -254,7 +254,7 @@ module API
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex, documentation: { example: 'mypkg-1.0-SNAPSHOT.pom' }
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' }
end
- route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true, basic_auth_personal_access_token: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
unprocessable_entity! if Gitlab::FIPS.enabled? && params[:file].md5
authorize_upload!
diff --git a/lib/api/members.rb b/lib/api/members.rb
index bdbdea70da0..56a15c41e1c 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -118,11 +118,8 @@ module API
post ":id/members", feature_category: feature_category do
source = find_source(source_type, params[:id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+
+ authorize_admin_source_member!(source_type, source)
create_service_params = params.merge(source: source)
@@ -148,11 +145,7 @@ module API
source = find_source(source_type, params.delete(:id))
member = source_members(source).find_by!(user_id: params[:user_id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_update_source_member!(source_type, member)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_update_source_member!(source_type, member)
result = ::Members::UpdateService
.new(current_user, declared_params(include_missing: false))
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 35fdcfe3ab0..d0c9400039a 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -86,6 +86,10 @@ module API
not_found! unless success
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: user_project, current_user: current_user)
+ .execute(merge_request, "unreviewed")
+
present_approval(merge_request)
end
diff --git a/lib/api/ml/mlflow/api_helpers.rb b/lib/api/ml/mlflow/api_helpers.rb
index 19ac0dbba1b..aefa156717c 100644
--- a/lib/api/ml/mlflow/api_helpers.rb
+++ b/lib/api/ml/mlflow/api_helpers.rb
@@ -12,6 +12,10 @@ module API
unauthorized! unless can?(current_user, :write_model_experiments, user_project)
end
+ def check_api_model_registry_read!
+ not_found! unless can?(current_user, :read_model_registry, user_project)
+ end
+
def resource_not_found!
render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404)
end
@@ -79,6 +83,10 @@ module API
candidate_repository.by_eid(eid) || resource_not_found!
end
+ def find_model(project, name)
+ ::Ml::FindModelService.new(project, name).execute || resource_not_found!
+ end
+
def packages_url
path = api_v4_projects_packages_generic_package_version_path(
id: user_project.id, package_name: '', file_name: ''
diff --git a/lib/api/ml/mlflow/entrypoint.rb b/lib/api/ml/mlflow/entrypoint.rb
index 3e0cb723580..7157d2a03f6 100644
--- a/lib/api/ml/mlflow/entrypoint.rb
+++ b/lib/api/ml/mlflow/entrypoint.rb
@@ -26,9 +26,6 @@ module API
status 200
authenticate!
-
- check_api_read!
- check_api_write! unless request.get? || request.head?
end
rescue_from ActiveRecord::ActiveRecordError do |e|
@@ -44,7 +41,9 @@ module API
end
namespace MLFLOW_API_PREFIX do
mount ::API::Ml::Mlflow::Experiments
+ mount ::API::Ml::Mlflow::ModelVersions
mount ::API::Ml::Mlflow::Runs
+ mount ::API::Ml::Mlflow::RegisteredModels
end
end
end
diff --git a/lib/api/ml/mlflow/experiments.rb b/lib/api/ml/mlflow/experiments.rb
index 614112f703b..1a501291941 100644
--- a/lib/api/ml/mlflow/experiments.rb
+++ b/lib/api/ml/mlflow/experiments.rb
@@ -9,6 +9,11 @@ module API
class Experiments < ::API::Base
feature_category :mlops
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ end
+
resource :experiments do
desc 'Fetch experiment by experiment_id' do
success Entities::Ml::Mlflow::GetExperiment
diff --git a/lib/api/ml/mlflow/model_versions.rb b/lib/api/ml/mlflow/model_versions.rb
new file mode 100644
index 00000000000..989b79e5774
--- /dev/null
+++ b/lib/api/ml/mlflow/model_versions.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module API
+ module Ml
+ module Mlflow
+ class ModelVersions < ::API::Base
+ feature_category :mlops
+
+ resource :model_versions do
+ desc 'Fetch model version by name and version' do
+ success Entities::Ml::Mlflow::ModelVersions::Responses::Get
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#get-modelversion'
+ end
+ params do
+ requires :name, type: String, desc: 'Model version name'
+ requires :version, type: String, desc: 'Model version number'
+ end
+ get 'get', urgency: :low do
+ check_api_model_registry_read!
+ resource_not_found! unless params[:name] && params[:version]
+ model_version = ::Ml::ModelVersions::GetModelVersionService.new(
+ user_project, params[:name], params[:version]
+ ).execute
+ resource_not_found! unless model_version
+ response = { model_version: model_version }
+ present response, with: Entities::Ml::Mlflow::ModelVersions::Responses::Get
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ml/mlflow/registered_models.rb b/lib/api/ml/mlflow/registered_models.rb
new file mode 100644
index 00000000000..18b705ad214
--- /dev/null
+++ b/lib/api/ml/mlflow/registered_models.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'mime/types'
+
+module API
+ # MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api
+ module Ml
+ module Mlflow
+ class RegisteredModels < ::API::Base
+ feature_category :mlops
+
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ check_api_model_registry_read!
+ end
+
+ resource 'registered-models' do
+ desc 'Creates a Registered Model.' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'MLFlow Registered Models map to GitLab Models. https://mlflow.org/docs/2.6.0/rest-api.html#create-registeredmodel'
+ end
+ params do
+ requires :name, type: String,
+ desc: 'Register models under this name.'
+ optional :description, type: String,
+ desc: 'Optional description for registered model.'
+ optional :tags, type: Array, desc: 'Additional metadata for registered model.'
+ end
+ post 'create', urgency: :low do
+ present ::Ml::CreateModelService.new(
+ user_project,
+ params[:name],
+ current_user,
+ params[:description],
+ params[:tags]
+ ).execute,
+ with: Entities::Ml::Mlflow::RegisteredModel,
+ root: :registered_model
+ rescue ActiveRecord::RecordInvalid
+ resource_already_exists!
+ end
+
+ desc 'Fetch a Registered Model by Name' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-registeredmodel'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String, default: '',
+ desc: 'Registered model unique name identifier, in reference to the project'
+ end
+ get 'get', urgency: :low do
+ present find_model(user_project, params[:name]), with: Entities::Ml::Mlflow::RegisteredModel,
+ root: :registered_model
+ end
+
+ desc 'Update a Registered Model by Name' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#update-registeredmodel'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String,
+ desc: 'Registered model unique name identifier, in reference to the project'
+ optional :description, type: String,
+ desc: 'Optional description for registered model.'
+ end
+ patch 'update', urgency: :low do
+ present ::Ml::UpdateModelService.new(find_model(user_project, params[:name]), params[:description]).execute,
+ with: Entities::Ml::Mlflow::RegisteredModel, root: :registered_model
+ end
+
+ desc 'Fetch the latest Model Version for the given Registered Model Name' do
+ success Entities::Ml::Mlflow::ModelVersions::Types::ModelVersion
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#get-latest-modelversions'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String,
+ desc: 'Registered model unique name identifier, in reference to the project'
+ end
+ post 'get-latest-versions', urgency: :low do
+ model = find_model(user_project, params[:name])
+
+ present [model.latest_version], with: Entities::Ml::Mlflow::ModelVersions::Types::ModelVersion,
+ root: :model_versions
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ml/mlflow/runs.rb b/lib/api/ml/mlflow/runs.rb
index ac052d8bff5..6716db21407 100644
--- a/lib/api/ml/mlflow/runs.rb
+++ b/lib/api/ml/mlflow/runs.rb
@@ -9,6 +9,11 @@ module API
class Runs < ::API::Base
feature_category :mlops
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ end
+
resource :runs do
desc 'Creates a Run.' do
success Entities::Ml::Mlflow::Run
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index 46b388a2fda..b061876b997 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -102,7 +102,7 @@ module API
end
def check_duplicate(file_params, symbol_package)
- return if symbol_package || Feature.disabled?(:nuget_duplicates_option, project_or_group.namespace)
+ return if symbol_package
service_params = file_params.merge(remote_url: params['package.remote_url'])
response = ::Packages::Nuget::CheckDuplicatesService.new(project_or_group, current_user, service_params).execute
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 9d234ca0593..de00b66ead3 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -72,11 +72,17 @@ module API
detail 'Roates a personal access token.'
success Entities::PersonalAccessTokenWithToken
end
+ params do
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
+ end
post ':id/rotate' do
token = PersonalAccessToken.find_by_id(params[:id])
if Ability.allowed?(current_user, :manage_user_personal_access_token, token&.user)
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
index 6b2ba41f013..7f531525870 100644
--- a/lib/api/project_packages.rb
+++ b/lib/api/project_packages.rb
@@ -46,6 +46,8 @@ module API
desc: 'Return packages of a certain type'
optional :package_name, type: String,
desc: 'Return packages with this name'
+ optional :package_version, type: String,
+ desc: 'Return packages with this version'
optional :include_versionless, type: Boolean,
desc: 'Returns packages without a version'
optional :status, type: String, values: Packages::Package.statuses.keys,
@@ -55,7 +57,7 @@ module API
get ':id/packages' do
packages = ::Packages::PackagesFinder.new(
user_project,
- declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless, :status)
+ declared_params.slice(:order_by, :sort, :package_type, :package_name, :package_version, :include_versionless, :status)
).execute
present paginate(packages), with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
index 5777b8754e7..b79348c87bf 100644
--- a/lib/api/project_repository_storage_moves.rb
+++ b/lib/api/project_repository_storage_moves.rb
@@ -8,6 +8,16 @@ module API
feature_category :gitaly
+ helpers do
+ extend ::Gitlab::Utils::Override
+
+ # Allow to move projects in hidden/pending_delete state
+ override :find_project_scopes
+ def find_project_scopes
+ Project
+ end
+ end
+
resource :project_repository_storage_moves do
desc 'Get a list of all project repository storage moves' do
detail 'This feature was introduced in GitLab 13.0.'
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ac28effea43..3b80fd125ca 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -159,6 +159,7 @@ module API
optional :topic_id, type: Integer, desc: 'Limit results to projects with the assigned topic given by the topic ID'
optional :updated_before, type: DateTime, desc: 'Return projects updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
optional :updated_after, type: DateTime, desc: 'Return projects updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :include_pending_delete, type: Boolean, desc: 'Include projects in pending delete state. Can only be set by admins'
use :optional_filter_params_ee
end
@@ -470,6 +471,7 @@ module API
optional :description, type: String, desc: 'The description that will be assigned to the fork', documentation: { example: 'Description' }
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork'
optional :mr_default_target_self, type: Boolean, desc: 'Merge requests of this forked project targets itself by default'
+ optional :branches, type: String, desc: 'Branches to fork'
end
post ':id/fork', feature_category: :source_code_management do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759')
@@ -489,6 +491,7 @@ module API
service = ::Projects::ForkService.new(user_project, current_user, fork_params)
+ not_found!('Source Branch') if fork_params[:branches].present? && !service.valid_fork_branch?(fork_params[:branches])
not_found!('Target Namespace') unless service.valid_fork_target?
forked_project = service.execute
@@ -792,7 +795,12 @@ module API
not_found!('Group Link') unless link
destroy_conditionally!(link) do
- ::Projects::GroupLinks::DestroyService.new(user_project, current_user).execute(link)
+ result = ::Projects::GroupLinks::DestroyService.new(user_project, current_user).execute(link)
+
+ if result.error?
+ status = :not_found if result.reason == :not_found
+ render_api_error!(result.message, status)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 027a11738d3..3313b3a87cd 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -280,6 +280,13 @@ module API
optional :requires_python, type: String, documentation: { example: '>=3.7' }
optional :md5_digest, type: String, documentation: { example: '900150983cd24fb0d6963f7d28e17f72' }
optional :sha256_digest, type: String, regexp: Gitlab::Regex.sha256_regex, documentation: { example: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' }
+ optional :metadata_version, type: String, documentation: { example: '2.3' }
+ optional :author_email, type: String, documentation: { example: 'cschultz@example.com, snoopy@peanuts.com' }
+ optional :description, type: String
+ optional :description_content_type, type: String,
+ documentation: { example: 'text/markdown; charset=UTF-8; variant=GFM' }
+ optional :summary, type: String, documentation: { example: 'A module for collecting votes from beagles.' }
+ optional :keywords, type: String, documentation: { example: 'dog,puppy,voting,election' }
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 5d056ade3da..83085b5b7e3 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -270,8 +270,6 @@ module API
.execute
if result[:status] == :success
- log_release_created_audit_event(result[:release])
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -317,9 +315,6 @@ module API
.execute
if result[:status] == :success
- log_release_updated_audit_event
- log_release_milestones_updated_audit_event if result[:milestones_updated]
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -350,8 +345,6 @@ module API
.execute
if result[:status] == :success
- log_release_deleted_audit_event
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -406,22 +399,6 @@ module API
Rack::Utils.parse_nested_query(@request.query_string)
end
- def log_release_created_audit_event(release)
- # extended in EE
- end
-
- def log_release_updated_audit_event
- # extended in EE
- end
-
- def log_release_deleted_audit_event
- # extended in EE
- end
-
- def log_release_milestones_updated_audit_event
- # extended in EE
- end
-
def release_cli?
request.env['HTTP_USER_AGENT']&.include?(RELEASE_CLI_USER_AGENT) == true
end
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index 1ad5bc8d421..752feb1455f 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -141,6 +141,10 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
requires :token_id, type: String, desc: "The ID of the token"
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
end
post ':id/access_tokens/:token_id/rotate' do
resource = find_source(source_type, params[:id])
@@ -149,7 +153,7 @@ module API
token = find_token(resource, params[:token_id]) if resource_accessible
if token
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 9120421fadf..7ad4ecd88b1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -204,6 +204,7 @@ module API
optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)'
optional :user_deactivation_emails_enabled, type: Boolean, desc: 'Send emails to users upon account deactivation'
optional :suggest_pipeline_enabled, type: Boolean, desc: 'Enable pipeline suggestion banner'
+ optional :enable_artifact_external_redirect_warning_page, type: Boolean, desc: 'Show the external redirect page that warns you about user-generated content in GitLab Pages'
optional :users_get_by_id_limit, type: Integer, desc: "Maximum number of calls to the /users/:id API per 10 minutes per user. Set to 0 for unlimited requests."
optional :runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for shared runners, in seconds'
optional :group_runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for group runners, in seconds'
diff --git a/lib/api/users.rb b/lib/api/users.rb
index dd9cb2ee019..5fa6d50581b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -34,10 +34,14 @@ module API
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def reorder_users(users)
- if params[:order_by] && params[:sort]
- users.reorder(order_options_with_tie_breaker)
- else
+ # Users#search orders by exact matches and handles pagination,
+ # so we should prioritize that.
+ if params[:search]
users
+ else
+ # Note that params[:order_by] and params[:sort] will always be present and
+ # default to "id" and "desc" as defined in `sort_params`.
+ users.reorder(order_options_with_tie_breaker)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
deleted file mode 100644
index 0ce5cdd06de..00000000000
--- a/lib/api/v3/github.rb
+++ /dev/null
@@ -1,289 +0,0 @@
-# frozen_string_literal: true
-
-# The endpoints by default return `404` in preparation for their removal
-# (also see comment above `#reversible_end_of_life!`).
-# https://gitlab.com/gitlab-org/gitlab/-/issues/362168
-#
-# These endpoints partially mimic Github API behavior in order to successfully
-# integrate with Jira Development Panel.
-module API
- module V3
- class Github < ::API::Base
- NO_SLASH_URL_PART_REGEX = %r{[^/]+}
- ENDPOINT_REQUIREMENTS = {
- namespace: NO_SLASH_URL_PART_REGEX,
- project: NO_SLASH_URL_PART_REGEX,
- username: NO_SLASH_URL_PART_REGEX
- }.freeze
-
- # Used to differentiate Jira Cloud requests from Jira Server requests
- # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
- # Jira Server user agent format: Jira DVCS Connector/version
- JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'
-
- GITALY_TIMEOUT_CACHE_KEY = 'api:v3:Gitaly-timeout-cache-key'
- GITALY_TIMEOUT_CACHE_EXPIRY = 1.day
-
- include PaginationParams
-
- feature_category :integrations
-
- before do
- authorize_jira_user_agent!(request)
- authenticate!
- reversible_end_of_life!
- end
-
- helpers do
- params :project_full_path do
- requires :namespace, type: String
- requires :project, type: String
- end
-
- # The endpoints in this class have been deprecated since 15.1.
- #
- # Due to uncertainty about the impact of a full removal in 16.0, all endpoints return `404`
- # by default but we allow customers to toggle a flag to reverse this breaking change.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/362168#note_1347692683.
- #
- # TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148.
- def reversible_end_of_life!
- not_found! unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty)
-
- Gitlab::IntegrationsLogger.info(
- user_id: current_user&.id,
- namespace: params[:namespace],
- project: params[:project],
- message: 'Deprecated Jira DVCS endpoint request'
- )
- end
-
- def authorize_jira_user_agent!(request)
- not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
- end
-
- def update_project_feature_usage_for(project)
- # Prevent errors on GitLab Geo not allowing
- # UPDATE statements to happen in GET requests.
- return if Gitlab::Database.read_only?
-
- project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
- end
-
- def jira_cloud?
- request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
- end
-
- def find_project_with_access(params)
- project = find_project!(
- ::Gitlab::Jira::Dvcs.restore_full_path(**params.slice(:namespace, :project).symbolize_keys)
- )
- not_found! unless can?(current_user, :read_code, project)
- project
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_merge_requests
- merge_requests = authorized_merge_requests.reorder(updated_at: :desc)
- paginate(merge_requests)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_merge_request_with_access(id, access_level = :read_merge_request)
- merge_request = authorized_merge_requests.find_by(id: id)
- not_found! unless can?(current_user, access_level, merge_request)
- merge_request
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def authorized_merge_requests
- MergeRequestsFinder.new(current_user, authorized_only: !current_user.can_read_all_resources?)
- .execute.with_jira_integration_associations
- end
-
- def authorized_merge_requests_for_project(project)
- MergeRequestsFinder
- .new(current_user, authorized_only: !current_user.can_read_all_resources?, project_id: project.id)
- .execute.with_jira_integration_associations
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_notes(noteable)
- # They're not presented on Jira Dev Panel ATM. A comments count with a
- # redirect link is presented.
- notes = paginate(noteable.notes.user.reorder(nil))
- notes.select { |n| n.readable_by?(current_user) }
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # Returns an empty Array instead of the Commit diff files for a period
- # of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts
- # for some Commit diffs.
- def diff_files(commit)
- cache_key = [
- GITALY_TIMEOUT_CACHE_KEY,
- commit.project.id,
- commit.cache_key
- ].join(':')
-
- return [] if Rails.cache.read(cache_key).present?
-
- begin
- commit.diffs.diff_files
- rescue GRPC::DeadlineExceeded => error
- # Gitaly fails to load diffs consistently for some commits. The other information
- # is still valuable for Jira. So we skip the loading and respond with a 200 excluding diffs
- # Remove this when https://gitlab.com/gitlab-org/gitaly/-/issues/3741 is fixed.
- Rails.cache.write(cache_key, 1, expires_in: GITALY_TIMEOUT_CACHE_EXPIRY)
- Gitlab::ErrorTracking.track_exception(error)
- []
- end
- end
- end
-
- resource :orgs do
- get ':namespace/repos' do
- present []
- end
- end
-
- resource :user do
- get :repos do
- present []
- end
- end
-
- resource :users do
- params do
- use :pagination
- end
-
- get ':namespace/repos' do
- namespace = Namespace.find_by_full_path(params[:namespace])
- not_found!('Namespace') unless namespace
-
- projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects
- projects = projects.in_namespace(namespace.self_and_descendants)
-
- projects_cte = Project.wrap_with_cte(projects)
- .eager_load_namespace_and_owner
- .with_route
-
- present paginate(projects_cte),
- with: ::API::Github::Entities::Repository,
- root_namespace: namespace.root_ancestor
- end
-
- get ':username' do
- forbidden! unless can?(current_user, :read_users_list)
- user = UsersFinder.new(current_user, { username: params[:username] }).execute.first
- not_found! unless user
- present user, with: ::API::Github::Entities::User
- end
- end
-
- # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead
- # "/api/v3/repos/<namespace>/<project>/pulls". This forces us into
- # returning _all_ Merge Requests from authorized projects (user is a member),
- # instead just the authorized MRs from a project.
- # Jira handles the filtering, presenting just MRs mentioning the Jira
- # issue ID on the MR title / description.
- resource :repos do
- # Keeping for backwards compatibility with old Jira integration instructions
- # so that users that do not change it will not suddenly have a broken integration
- get '/-/jira/pulls' do
- present find_merge_requests, with: ::API::Github::Entities::PullRequest
- end
-
- get '/-/jira/events' do
- present []
- end
-
- params do
- use :project_full_path
- end
- # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/337269
- get ':namespace/:project/pulls', urgency: :low do
- user_project = find_project_with_access(params)
-
- merge_requests = authorized_merge_requests_for_project(user_project)
-
- present paginate(merge_requests), with: ::API::Github::Entities::PullRequest
- end
-
- params do
- use :project_full_path
- end
- get ':namespace/:project/pulls/:id' do
- merge_request = find_merge_request_with_access(params[:id])
-
- present merge_request, with: ::API::Github::Entities::PullRequest
- end
-
- # In Github, each Merge Request is automatically also an issue.
- # Therefore we return its comments here.
- # It'll present _just_ the comments counting with a link to GitLab on
- # Jira dev panel, not the actual note content.
- get ':namespace/:project/issues/:id/comments' do
- merge_request = find_merge_request_with_access(params[:id])
-
- present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment
- end
-
- # This refer to "review" comments but Jira dev panel doesn't seem to
- # present it accordingly.
- get ':namespace/:project/pulls/:id/comments' do
- present []
- end
-
- # Commits are not presented within "Pull Requests" modal on Jira dev
- # panel.
- get ':namespace/:project/pulls/:id/commits' do
- present []
- end
-
- # Self-hosted Jira (tested on 7.11.1) requests this endpoint right
- # after fetching branches.
- get ':namespace/:project/events' do
- user_project = find_project_with_access(params)
-
- merge_requests = authorized_merge_requests_for_project(user_project)
-
- present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
- end
-
- params do
- use :project_full_path
- use :pagination
- end
- # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/337268
- get ':namespace/:project/branches', urgency: :low do
- user_project = find_project_with_access(params)
-
- update_project_feature_usage_for(user_project)
-
- next [] unless user_project.repo_exists?
-
- branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
-
- present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
- end
-
- params do
- use :project_full_path
- end
- get ':namespace/:project/commits/:sha' do
- user_project = find_project_with_access(params)
-
- commit = user_project.commit(params[:sha])
- not_found! 'Commit' unless commit
-
- present commit, with: ::API::Github::Entities::RepoCommit, diff_files: diff_files(commit)
- end
- end
- end
- end
-end
diff --git a/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb b/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb
new file mode 100644
index 00000000000..38af85dc0c7
--- /dev/null
+++ b/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module API
+ module VsCode
+ module Settings
+ module Entities
+ class VsCodeSettingReference < Grape::Entity
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :url do |setting|
+ expose_path(api_v4_vscode_settings_sync_v1_resource_path(
+ resource_name: setting[:setting_type],
+ id: setting[:uuid]
+ ))
+ end
+ expose :created do |setting|
+ setting[:updated_at]&.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/vs_code/settings/vs_code_settings_sync.rb b/lib/api/vs_code/settings/vs_code_settings_sync.rb
index dc22496e380..1e53125a3aa 100644
--- a/lib/api/vs_code/settings/vs_code_settings_sync.rb
+++ b/lib/api/vs_code/settings/vs_code_settings_sync.rb
@@ -8,6 +8,14 @@ module API
feature_category :web_ide
+ helpers do
+ def find_settings
+ return [DEFAULT_MACHINE] if params[:resource_name] == DEFAULT_MACHINE[:setting_type]
+
+ SettingsFinder.new(current_user, [params[:resource_name]]).execute
+ end
+ end
+
before do
authenticate!
@@ -21,6 +29,9 @@ module API
desc 'Get the settings manifest for Settings Sync' do
success [Entities::VsCodeManifest]
+ failure [
+ { code: 401, message: '401 Unauthorized' }
+ ]
tags %w[vscode]
end
get '/v1/manifest' do
@@ -31,44 +42,71 @@ module API
end
desc 'Get a specific setting resource' do
- success [Entities::VsCodeSetting]
+ success [
+ Entities::VsCodeSetting,
+ { code: 204, message: 'No content' }
+ ]
+ failure [
+ { code: 400, message: '400 bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
tags %w[vscode]
end
params do
- requires :resource_name, type: String, desc: 'Name of the resource such as settings'
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
requires :id, type: String, desc: 'ID of the resource to retrieve'
end
get '/v1/resource/:resource_name/:id' do
- authenticate!
-
- setting_name = params[:resource_name]
- setting = nil
+ settings = find_settings
- if params[:resource_name] == 'machines'
- setting = DEFAULT_MACHINE
- else
- settings = SettingsFinder.new(current_user, [setting_name]).execute
- setting = settings.first if settings.present?
- end
-
- if setting.nil?
+ if settings.blank?
status :no_content
header :etag, NO_CONTENT_ETAG
body false
else
+ # This endpoint does not use the :id parameter
+ # because the first iteration of this API only
+ # supports storing a single record of a given setting_type.
+ # We can rely on obtaining the first record of the setting
+ # result.
+ setting = settings.first
header :etag, setting[:uuid]
presenter = VsCodeSettingPresenter.new setting
present presenter, with: Entities::VsCodeSetting
end
end
- desc 'Update a specific setting'
+ desc 'Get a list of references to one or more vscode setting resources' do
+ success [Entities::VsCodeSettingReference]
+ failure [
+ { code: 400, message: '400 bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ tags %w[vscode]
+ end
params do
- requires :resource_name, type: String, desc: 'Name of the resource such as settings'
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
end
- post '/v1/resource/:resource_name' do
- authenticate!
+ get '/v1/resource/:resource_name' do
+ settings = find_settings
+ present settings, with: Entities::VsCodeSettingReference
+ end
+
+ desc 'Creates or updates a specific setting' do
+ success [{ code: 200, message: 'OK' }]
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ end
+ params do
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
+ end
+ post '/v1/resource/:resource_name' do
response = CreateOrUpdateService.new(current_user: current_user, params: {
content: params[:content],
version: params[:version],
@@ -83,6 +121,19 @@ module API
error!(response.message, 400)
end
end
+
+ desc 'Deletes all user vscode setting resources' do
+ success [{ code: 200, message: 'OK' }]
+ failure [
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ tags %w[vscode]
+ end
+ delete '/v1/collection' do
+ DeleteService.new(current_user: current_user).execute
+
+ present "OK"
+ end
end
end
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 2058f5de706..a7408512102 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -85,6 +85,9 @@ module API
end
params do
requires :title, type: String, desc: 'Title of a wiki page'
+ optional :front_matter, type: Hash do
+ optional :title, type: String, desc: 'Front matter title of a wiki page'
+ end
requires :content, type: String, desc: 'Content of a wiki page'
use :common_wiki_page_params
end
@@ -112,6 +115,9 @@ module API
end
params do
optional :title, type: String, desc: 'Title of a wiki page'
+ optional :front_matter, type: Hash do
+ optional :title, type: String, desc: 'Front matter title of a wiki page'
+ end
optional :content, type: String, desc: 'Content of a wiki page'
use :common_wiki_page_params
at_least_one_of :content, :title, :format
diff --git a/lib/atlassian/jira_connect/jira_user.rb b/lib/atlassian/jira_connect/jira_user.rb
index 57ceb8fdf13..051165474af 100644
--- a/lib/atlassian/jira_connect/jira_user.rb
+++ b/lib/atlassian/jira_connect/jira_user.rb
@@ -3,15 +3,17 @@
module Atlassian
module JiraConnect
class JiraUser
+ ADMIN_GROUPS = %w[site-admins org-admins].freeze
+
def initialize(data)
@data = data
end
- def site_admin?
+ def jira_admin?
groups = @data.dig('groups', 'items')
return false unless groups
- groups.any? { |g| g['name'] == 'site-admins' }
+ groups.any? { |group| ADMIN_GROUPS.include?(group['name']) }
end
end
end
diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb
index 5b55c2cbdf7..366151a63b4 100644
--- a/lib/backup/gitaly_backup.rb
+++ b/lib/backup/gitaly_backup.rb
@@ -92,7 +92,7 @@ module Backup
args += ['-id', backup_id] if backup_id
when :restore
args += ['-remove-all-repositories', remove_all_repositories.join(',')] if remove_all_repositories
- args += ['-id', backup_id] if backup_id && server_side?
+ args += ['-id', backup_id] if backup_id
end
args
diff --git a/lib/banzai/filter/asset_proxy_filter.rb b/lib/banzai/filter/asset_proxy_filter.rb
index 512c55381ec..eae69700465 100644
--- a/lib/banzai/filter/asset_proxy_filter.rb
+++ b/lib/banzai/filter/asset_proxy_filter.rb
@@ -23,7 +23,8 @@ module Banzai
begin
uri = URI.parse(original_src)
- next if uri.host.nil? && !original_src.start_with?('///')
+ # Skip URLs like `/path.ext` or `path.ext` which are relative to the current host
+ next if uri.relative? && uri.host.nil? && original_src.match(%r{\A/*})[0].length < 2
next if asset_host_allowed?(uri.host)
rescue StandardError
# Ignored
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 3161e030194..511da4b6ba5 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -93,10 +93,20 @@ module Banzai
end
def render_nodes_limit_reached?(count)
+ return false if wiki?
+ return false if blob?
return false unless settings.math_rendering_limits_enabled?
count >= RENDER_NODES_LIMIT
end
+
+ def wiki?
+ context[:wiki].present?
+ end
+
+ def blob?
+ context[:text_source] == :blob
+ end
end
end
end
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index a3784004087..3fcb36c4714 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -65,13 +65,10 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- cross_join_issue = "https://gitlab.com/gitlab-org/gitlab/-/issues/417466"
- Gitlab::Database.allow_cross_joins_across_databases(url: cross_join_issue) do
- @namespaces ||= Namespace.eager_load(:owner, :route)
- .where_full_path_in(usernames)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
+ @namespaces ||= Namespace.preload(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index ec96181e7f1..bba5a7dfd09 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -6,9 +6,9 @@ module Banzai
self.reference_type = :user
def referenced_by(nodes, options = {})
- group_ids = []
- user_ids = []
- project_ids = []
+ group_ids = Set.new
+ user_ids = Set.new
+ project_ids = Set.new
nodes.each do |node|
if node.has_attribute?('data-group')
@@ -20,8 +20,10 @@ module Banzai
end
end
- find_users_for_groups(group_ids) | find_users(user_ids) |
- find_users_for_projects(project_ids)
+ user_ids += find_user_ids_for_groups(group_ids)
+ user_ids += find_user_ids_for_projects(project_ids)
+
+ find_users(user_ids)
end
def nodes_visible_to_user(user, nodes)
@@ -49,20 +51,6 @@ module Banzai
visible + super(current_user, remaining)
end
- # Check if project belongs to a group which
- # user can read.
- def can_read_group_reference?(node, user, groups)
- node_group = groups[node]
-
- node_group && can?(user, :read_group, node_group)
- end
-
- def can_read_project_reference?(node)
- node_id = node.attr('data-project').to_i
-
- project_for_node(node)&.id == node_id
- end
-
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
@@ -88,28 +76,44 @@ module Banzai
end
end
+ private
+
+ # Check if project belongs to a group which
+ # user can read.
+ def can_read_group_reference?(node, user, groups)
+ node_group = groups[node]
+
+ node_group && can?(user, :read_group, node_group)
+ end
+
+ def can_read_project_reference?(node)
+ node_id = node.attr('data-project').to_i
+
+ project_for_node(node)&.id == node_id
+ end
+
def find_users(ids)
return [] if ids.empty?
collection_objects_for_ids(User, ids)
end
- def find_users_for_groups(ids)
- return [] if ids.empty?
+ def find_user_ids_for_groups(group_ids)
+ return [] if group_ids.empty?
- cross_join_issue = "https://gitlab.com/gitlab-org/gitlab/-/issues/417466"
- ::Gitlab::Database.allow_cross_joins_across_databases(url: cross_join_issue) do
- User.joins(:group_members).where(members: {
- source_id: Namespace.where(id: ids).where('mentions_disabled IS NOT TRUE').select(:id)
- }).to_a
- end
+ GroupMember
+ .of_groups(Group.id_in(group_ids).where('mentions_disabled IS NOT TRUE'))
+ .non_request
+ .non_invite
+ .non_minimal_access
+ .distinct
+ .pluck(:user_id)
end
- def find_users_for_projects(ids)
- return [] if ids.empty?
+ def find_user_ids_for_projects(project_ids)
+ return [] if project_ids.empty?
- collection_objects_for_ids(Project, ids)
- .flat_map { |p| p.team.members.to_a }
+ ProjectAuthorization.for_project(project_ids).pluck(:user_id)
end
def can_read_reference?(user, ref_project, node)
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
index 34dbf9ad22d..11ce0c26677 100644
--- a/lib/bitbucket/representation/pull_request_comment.rb
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -31,6 +31,10 @@ module Bitbucket
raw.key?('parent')
end
+ def deleted?
+ raw.fetch('deleted', false)
+ end
+
private
def inline
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 8d5b15e299a..3764d116a36 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -59,6 +59,10 @@ module Bitbucket
end
end
+ def default_branch
+ raw.dig('mainbranch', 'name')
+ end
+
def to_s
full_name
end
diff --git a/lib/bulk_imports/clients/graphql.rb b/lib/bulk_imports/clients/graphql.rb
index 94bbdfaa681..3055c8d24ce 100644
--- a/lib/bulk_imports/clients/graphql.rb
+++ b/lib/bulk_imports/clients/graphql.rb
@@ -4,11 +4,14 @@ module BulkImports
module Clients
class Graphql
class HTTP < Graphlient::Adapters::HTTP::Adapter
+ REQUEST_TIMEOUT = 60
+
def execute(document:, operation_name: nil, variables: {}, context: {})
response = ::Gitlab::HTTP.post(
url,
headers: headers,
follow_redirects: false,
+ timeout: REQUEST_TIMEOUT,
body: {
query: document.to_query_string,
operationName: operation_name,
diff --git a/lib/bulk_imports/common/pipelines/entity_finisher.rb b/lib/bulk_imports/common/pipelines/entity_finisher.rb
index fa09f36fdd6..723359aa438 100644
--- a/lib/bulk_imports/common/pipelines/entity_finisher.rb
+++ b/lib/bulk_imports/common/pipelines/entity_finisher.rb
@@ -30,8 +30,7 @@ module BulkImports
source_full_path: entity.source_full_path,
pipeline_class: self.class.name,
message: "Entity #{entity.status_name}",
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
)
::BulkImports::FinishProjectImportWorker.perform_async(entity.project_id) if entity.project?
@@ -42,7 +41,7 @@ module BulkImports
attr_reader :context, :entity, :trackers
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def all_other_trackers_failed?
diff --git a/lib/bulk_imports/logger.rb b/lib/bulk_imports/logger.rb
new file mode 100644
index 00000000000..be15c050770
--- /dev/null
+++ b/lib/bulk_imports/logger.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Logger < ::Gitlab::Import::Logger
+ IMPORTER_NAME = 'gitlab_migration'
+
+ def default_attributes
+ super.merge(importer: IMPORTER_NAME)
+ end
+ end
+end
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index 89ae66938af..07118c3b55c 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -135,7 +135,7 @@ module BulkImports
bulk_import_entity_id: tracker.entity.id,
pipeline_class: tracker.pipeline_name,
exception_class: 'RecordInvalid',
- exception_message: record.errors.full_messages.to_sentence.truncate(255),
+ exception_message: record.errors.full_messages.to_sentence,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
end
diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb
index 666916f8758..e2a14c35e79 100644
--- a/lib/bulk_imports/pipeline/runner.rb
+++ b/lib/bulk_imports/pipeline/runner.rb
@@ -17,7 +17,7 @@ module BulkImports
if extracted_data
extracted_data.each_with_index do |entry, index|
raw_entry = entry.dup
- next if Feature.enabled?(:bulk_import_idempotent_workers) && already_processed?(raw_entry, index)
+ next if already_processed?(raw_entry, index)
transformers.each do |transformer|
entry = run_pipeline_step(:transformer, transformer.class.name) do
@@ -25,11 +25,11 @@ module BulkImports
end
end
- run_pipeline_step(:loader, loader.class.name) do
+ run_pipeline_step(:loader, loader.class.name, entry) do
loader.load(context, entry)
end
- save_processed_entry(raw_entry, index) if Feature.enabled?(:bulk_import_idempotent_workers)
+ save_processed_entry(raw_entry, index)
end
tracker.update!(
@@ -40,6 +40,14 @@ module BulkImports
run_pipeline_step(:after_run) do
after_run(extracted_data)
end
+
+ # For batches, `#on_finish` is called once within `FinishBatchedPipelineWorker`
+ # after all batches have completed.
+ unless tracker.batched?
+ run_pipeline_step(:on_finish) do
+ on_finish
+ end
+ end
end
info(message: 'Pipeline finished')
@@ -47,9 +55,11 @@ module BulkImports
skip!('Skipping pipeline due to failed entity')
end
+ def on_finish; end
+
private # rubocop:disable Lint/UselessAccessModifier
- def run_pipeline_step(step, class_name = nil)
+ def run_pipeline_step(step, class_name = nil, entry = nil)
raise MarkedAsFailedError if context.entity.failed?
info(pipeline_step: step, step_class: class_name)
@@ -65,11 +75,11 @@ module BulkImports
rescue BulkImports::NetworkError => e
raise BulkImports::RetryPipelineError.new(e.message, e.retry_delay) if e.retriable?(context.tracker)
- log_and_fail(e, step)
+ log_and_fail(e, step, entry)
rescue BulkImports::RetryPipelineError
raise
rescue StandardError => e
- log_and_fail(e, step)
+ log_and_fail(e, step, entry)
end
def extracted_data_from
@@ -95,8 +105,8 @@ module BulkImports
run if extracted_data.has_next_page?
end
- def log_and_fail(exception, step)
- log_import_failure(exception, step)
+ def log_and_fail(exception, step, entry = nil)
+ log_import_failure(exception, step, entry)
if abort_on_failure?
tracker.fail_op!
@@ -114,16 +124,21 @@ module BulkImports
tracker.skip!
end
- def log_import_failure(exception, step)
+ def log_import_failure(exception, step, entry)
failure_attributes = {
bulk_import_entity_id: context.entity.id,
pipeline_class: pipeline,
pipeline_step: step,
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
}
+ if entry
+ failure_attributes[:source_url] = BulkImports::SourceUrlBuilder.new(context, entry).url
+ failure_attributes[:source_title] = entry.try(:title) || entry.try(:name)
+ end
+
log_exception(
exception,
log_params(
@@ -154,8 +169,7 @@ module BulkImports
source_full_path: context.entity.source_full_path,
pipeline_class: pipeline,
context_extra: context.extra,
- source_version: context.entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: context.entity.bulk_import.source_version_info.to_s
}
defaults
@@ -164,7 +178,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
diff --git a/lib/bulk_imports/pipeline_schema_info.rb b/lib/bulk_imports/pipeline_schema_info.rb
new file mode 100644
index 00000000000..df35a3569d6
--- /dev/null
+++ b/lib/bulk_imports/pipeline_schema_info.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class PipelineSchemaInfo
+ def initialize(pipeline_class, portable_class)
+ @pipeline_class = pipeline_class
+ @portable_class = portable_class
+ end
+
+ def db_schema
+ return unless relation
+ return unless association
+
+ Gitlab::Database::GitlabSchema.tables_to_schema[association.table_name]
+ end
+
+ def db_table
+ return unless relation
+ return unless association
+
+ association.table_name
+ end
+
+ private
+
+ attr_reader :pipeline_class, :portable_class
+
+ def relation
+ @relation ||= pipeline_class.try(:relation)
+ end
+
+ def association
+ @association ||= portable_class.reflect_on_association(relation)
+ end
+ end
+end
diff --git a/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb b/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
index 264bda6e654..fe5c61e81a3 100644
--- a/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
@@ -10,8 +10,8 @@ module BulkImports
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
- def after_run(_)
- context.portable.merge_requests.set_latest_merge_request_diff_ids!
+ def on_finish
+ ::Projects::ImportExport::AfterImportMergeRequestsWorker.perform_async(context.portable.id)
end
end
end
diff --git a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
index c77e53b9aec..433419f4c5c 100644
--- a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
@@ -10,9 +10,7 @@ module BulkImports
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
- def after_run(_context)
- super
-
+ def on_finish
portable.releases.find_each do |release|
create_release_evidence(release)
end
diff --git a/lib/bulk_imports/source_url_builder.rb b/lib/bulk_imports/source_url_builder.rb
new file mode 100644
index 00000000000..875b2eae9f7
--- /dev/null
+++ b/lib/bulk_imports/source_url_builder.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class SourceUrlBuilder
+ ALLOWED_RELATIONS = %w[
+ issues
+ merge_requests
+ epics
+ milestones
+ ].freeze
+
+ attr_reader :context, :entity, :entry
+
+ # @param [BulkImports::Pipeline::Context] context
+ # @param [ApplicationRecord] entry
+ def initialize(context, entry)
+ @context = context
+ @entity = context.entity
+ @entry = entry
+ end
+
+ # Builds a source URL for the given entry if iid is present
+ def url
+ return unless entry.is_a?(ApplicationRecord)
+ return unless iid
+ return unless ALLOWED_RELATIONS.include?(relation)
+
+ File.join(source_instance_url, group_prefix, source_full_path, '-', relation, iid.to_s)
+ end
+
+ private
+
+ def iid
+ @iid ||= entry.try(:iid)
+ end
+
+ def relation
+ @relation ||= context.tracker.pipeline_class.relation
+ end
+
+ def source_instance_url
+ @source_instance_url ||= context.bulk_import.configuration.url
+ end
+
+ def source_full_path
+ @source_full_path ||= entity.source_full_path
+ end
+
+ # Group milestone (or epic) url is /groups/:group_path/-/milestones/:iid
+ # Project milestone url is /:project_path/-/milestones/:iid
+ def group_prefix
+ return '' if entity.project?
+
+ entity.pluralized_name
+ end
+ end
+end
diff --git a/lib/click_house/migration.rb b/lib/click_house/migration.rb
new file mode 100644
index 00000000000..410a7ec86bc
--- /dev/null
+++ b/lib/click_house/migration.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ class Migration
+ cattr_accessor :verbose, :client_configuration
+ attr_accessor :name, :version
+
+ class << self
+ attr_accessor :delegate
+ end
+
+ def initialize(name = self.class.name, version = nil)
+ @name = name
+ @version = version
+ end
+
+ self.client_configuration = ClickHouse::Client.configuration
+ self.verbose = true
+ # instantiate the delegate object after initialize is defined
+ self.delegate = new
+
+ MIGRATION_FILENAME_REGEXP = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/
+
+ def database
+ self.class.constants.include?(:SCHEMA) ? self.class.const_get(:SCHEMA, false) : :main
+ end
+
+ def execute(query)
+ ClickHouse::Client.execute(query, database, self.class.client_configuration)
+ end
+
+ def up
+ self.class.delegate = self
+
+ return unless self.class.respond_to?(:up)
+
+ self.class.up
+ end
+
+ def down
+ self.class.delegate = self
+
+ return unless self.class.respond_to?(:down)
+
+ self.class.down
+ end
+
+ # Execute this migration in the named direction
+ def migrate(direction)
+ return unless respond_to?(direction)
+
+ case direction
+ when :up then announce 'migrating'
+ when :down then announce 'reverting'
+ end
+
+ time = Benchmark.measure do
+ exec_migration(direction)
+ end
+
+ case direction
+ when :up then announce format("migrated (%.4fs)", time.real)
+ write
+ when :down then announce format("reverted (%.4fs)", time.real)
+ write
+ end
+ end
+
+ private
+
+ def exec_migration(direction)
+ # noinspection RubyCaseWithoutElseBlockInspection
+ case direction
+ when :up then up
+ when :down then down
+ end
+ end
+
+ def write(text = '')
+ $stdout.puts(text) if verbose
+ end
+
+ def announce(message)
+ text = "#{version} #{name}: #{message}"
+ length = [0, 75 - text.length].max
+ write format('== %s %s', text, '=' * length)
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migration_context.rb b/lib/click_house/migration_support/migration_context.rb
new file mode 100644
index 00000000000..6e4dd2a97c2
--- /dev/null
+++ b/lib/click_house/migration_support/migration_context.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ # MigrationContext sets the context in which a migration is run.
+ #
+ # A migration context requires the path to the migrations is set
+ # in the +migrations_paths+ parameter. Optionally a +schema_migration+
+ # class can be provided. For most applications, +SchemaMigration+ is
+ # sufficient. Multiple database applications need a +SchemaMigration+
+ # per primary database.
+ class MigrationContext
+ attr_reader :migrations_paths, :schema_migration
+
+ def initialize(migrations_paths, schema_migration)
+ @migrations_paths = migrations_paths
+ @schema_migration = schema_migration
+ end
+
+ def up(target_version = nil, &block)
+ selected_migrations = block ? migrations.select(&block) : migrations
+
+ migrate(:up, selected_migrations, target_version)
+ end
+
+ def down(target_version = nil, &block)
+ selected_migrations = block ? migrations.select(&block) : migrations
+
+ migrate(:down, selected_migrations, target_version)
+ end
+
+ private
+
+ def migrate(direction, selected_migrations, target_version = nil)
+ ClickHouse::MigrationSupport::Migrator.new(
+ direction,
+ selected_migrations,
+ schema_migration,
+ target_version
+ ).migrate
+ end
+
+ def migrations
+ migrations = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+
+ raise ClickHouse::MigrationSupport::IllegalMigrationNameError, file unless version
+
+ version = version.to_i
+ name = name.camelize
+
+ MigrationProxy.new(name, version, file, scope)
+ end
+
+ migrations.sort_by(&:version)
+ end
+
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
+
+ def parse_migration_filename(filename)
+ File.basename(filename).scan(ClickHouse::Migration::MIGRATION_FILENAME_REGEXP).first
+ end
+ end
+
+ # MigrationProxy is used to defer loading of the actual migration classes
+ # until they are needed
+ MigrationProxy = Struct.new(:name, :version, :filename, :scope) do
+ def initialize(name, version, filename, scope)
+ super
+ @migration = nil
+ end
+
+ def basename
+ File.basename(filename)
+ end
+
+ delegate :migrate, :announce, :write, :database, to: :migration
+
+ private
+
+ def migration
+ @migration ||= load_migration
+ end
+
+ def load_migration
+ require(File.expand_path(filename))
+ name.constantize.new(name, version)
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migration_error.rb b/lib/click_house/migration_support/migration_error.rb
new file mode 100644
index 00000000000..0638d487e37
--- /dev/null
+++ b/lib/click_house/migration_support/migration_error.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class MigrationError < StandardError
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
+ class IllegalMigrationNameError < MigrationError
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super('Illegal name for migration.')
+ end
+ end
+ end
+
+ IrreversibleMigration = Class.new(MigrationError)
+
+ class DuplicateMigrationVersionError < MigrationError
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super('Duplicate migration version error.')
+ end
+ end
+ end
+
+ class DuplicateMigrationNameError < MigrationError
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super('Duplicate migration name.')
+ end
+ end
+ end
+
+ class UnknownMigrationVersionError < MigrationError
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super('Unknown migration version.')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migrator.rb b/lib/click_house/migration_support/migrator.rb
new file mode 100644
index 00000000000..5c67b3a5ff1
--- /dev/null
+++ b/lib/click_house/migration_support/migrator.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class Migrator
+ class << self
+ attr_accessor :migrations_paths
+ end
+
+ attr_accessor :logger
+
+ self.migrations_paths = ["db/click_house/migrate"]
+
+ def initialize(direction, migrations, schema_migration, target_version = nil, logger = Gitlab::AppLogger)
+ @direction = direction
+ @target_version = target_version
+ @migrated_versions = {}
+ @migrations = migrations
+ @schema_migration = schema_migration
+ @logger = logger
+
+ validate(@migrations)
+
+ migrations.map(&:database).uniq.each do |database|
+ @schema_migration.create_table(database)
+ end
+ end
+
+ def current_version
+ @migrated_versions.values.flatten.max || 0
+ end
+
+ def current_migration
+ migrations.detect { |m| m.version == current_version }
+ end
+ alias_method :current, :current_migration
+
+ def run
+ run_without_lock
+ end
+
+ def migrate
+ migrate_without_lock
+ end
+
+ def runnable
+ runnable = migrations[start..finish]
+
+ if up?
+ runnable.reject { |m| ran?(m) }
+ else
+ # skip the last migration if we're headed down, but not ALL the way down
+ runnable.pop if target
+ runnable.find_all { |m| ran?(m) }
+ end
+ end
+
+ def migrations
+ down? ? @migrations.reverse : @migrations.sort_by(&:version)
+ end
+
+ def pending_migrations(database)
+ already_migrated = migrated(database)
+
+ migrations.reject { |m| already_migrated.include?(m.version) }
+ end
+
+ def migrated(database)
+ @migrated_versions[database] || load_migrated(database)
+ end
+
+ def load_migrated(database)
+ @migrated_versions[database] = Set.new(@schema_migration.all_versions(database).map(&:to_i))
+ end
+
+ private
+
+ # Used for running a specific migration.
+ def run_without_lock
+ migration = migrations.detect { |m| m.version == @target_version }
+
+ raise ClickHouse::MigrationSupport::UnknownMigrationVersionError, @target_version if migration.nil?
+
+ execute_migration(migration)
+ end
+
+ # Used for running multiple migrations up to or down to a certain value.
+ def migrate_without_lock
+ raise ClickHouse::MigrationSupport::UnknownMigrationVersionError, @target_version if invalid_target?
+
+ runnable.each(&method(:execute_migration)) # rubocop: disable Performance/MethodObjectAsBlock -- Execute through proxy
+ end
+
+ def ran?(migration)
+ migrated(migration.database).include?(migration.version.to_i)
+ end
+
+ # Return true if a valid version is not provided.
+ def invalid_target?
+ return unless @target_version
+ return if @target_version == 0
+
+ !target
+ end
+
+ def execute_migration(migration)
+ database = migration.database
+
+ return if down? && migrated(database).exclude?(migration.version.to_i)
+ return if up? && migrated(database).include?(migration.version.to_i)
+
+ logger.info "Migrating to #{migration.name} (#{migration.version})" if logger
+
+ migration.migrate(@direction)
+ record_version_state_after_migrating(database, migration.version)
+ rescue StandardError => e
+ msg = "An error has occurred, all later migrations canceled:\n\n#{e}"
+ raise StandardError, msg, e.backtrace
+ end
+
+ def target
+ migrations.detect { |m| m.version == @target_version }
+ end
+
+ def finish
+ migrations.index(target) || (migrations.size - 1)
+ end
+
+ def start
+ up? ? 0 : (migrations.index(current) || 0)
+ end
+
+ def validate(migrations)
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ raise ClickHouse::MigrationSupport::DuplicateMigrationNameError, name if name
+
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ raise ClickHouse::MigrationSupport::DuplicateMigrationVersionError, version if version
+ end
+
+ def record_version_state_after_migrating(database, version)
+ if down?
+ migrated(database).delete(version)
+ @schema_migration.create!(database, version: version.to_s, active: 0)
+ else
+ migrated(database) << version
+ @schema_migration.create!(database, version: version.to_s)
+ end
+ end
+
+ def up?
+ @direction == :up
+ end
+
+ def down?
+ @direction == :down
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/schema_migration.rb b/lib/click_house/migration_support/schema_migration.rb
new file mode 100644
index 00000000000..e82debbad0d
--- /dev/null
+++ b/lib/click_house/migration_support/schema_migration.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class SchemaMigration
+ class_attribute :table_name_prefix, instance_writer: false, default: ''
+ class_attribute :table_name_suffix, instance_writer: false, default: ''
+ class_attribute :schema_migrations_table_name, instance_accessor: false, default: 'schema_migrations'
+
+ class << self
+ TABLE_EXISTS_QUERY = <<~SQL.squish
+ SELECT 1 FROM system.tables
+ WHERE name = {table_name: String} AND database = {database_name: String}
+ SQL
+
+ def primary_key
+ 'version'
+ end
+
+ def table_name
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
+ end
+
+ def table_exists?(database, configuration = ClickHouse::Migration.client_configuration)
+ database_name = configuration.databases[database]&.database
+ return false unless database_name
+
+ placeholders = { table_name: table_name, database_name: database_name }
+ query = ClickHouse::Client::Query.new(raw_query: TABLE_EXISTS_QUERY, placeholders: placeholders)
+
+ ClickHouse::Client.select(query, database, configuration).any?
+ end
+
+ def create_table(database, configuration = ClickHouse::Migration.client_configuration)
+ return if table_exists?(database, configuration)
+
+ query = <<~SQL
+ CREATE TABLE #{table_name} (
+ version LowCardinality(String),
+ active UInt8 NOT NULL DEFAULT 1,
+ applied_at DateTime64(6, 'UTC') NOT NULL DEFAULT now64()
+ )
+ ENGINE = ReplacingMergeTree(applied_at)
+ PRIMARY KEY(version)
+ ORDER BY (version)
+ SQL
+
+ ClickHouse::Client.execute(query, database, configuration)
+ end
+
+ def all_versions(database)
+ query = <<~SQL
+ SELECT version FROM #{table_name} FINAL
+ WHERE active = 1
+ ORDER BY (version)
+ SQL
+
+ ClickHouse::Client.select(query, database, ClickHouse::Migration.client_configuration).pluck('version')
+ end
+
+ def create!(database, **args)
+ insert_sql = <<~SQL
+ INSERT INTO #{table_name} (#{args.keys.join(',')}) VALUES (#{args.values.join(',')})
+ SQL
+
+ ClickHouse::Client.execute(insert_sql, database, ClickHouse::Migration.client_configuration)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/click_house/models/audit_event.rb b/lib/click_house/models/audit_event.rb
new file mode 100644
index 00000000000..a31b4a45298
--- /dev/null
+++ b/lib/click_house/models/audit_event.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module Models
+ class AuditEvent < ClickHouse::Models::BaseModel
+ def self.table_name
+ 'audit_events'
+ end
+
+ def by_entity_type(entity_type)
+ where(entity_type: entity_type)
+ end
+
+ def by_entity_id(entity_id)
+ where(entity_id: entity_id)
+ end
+
+ def by_author_id(author_id)
+ where(author_id: author_id)
+ end
+
+ def by_entity_username(username)
+ where(entity_id: self.class.find_user_id(username))
+ end
+
+ def by_author_username(username)
+ where(author_id: self.class.find_user_id(username))
+ end
+
+ def self.by_entity_type(entity_type)
+ new.by_entity_type(entity_type)
+ end
+
+ def self.by_entity_id(entity_id)
+ new.by_entity_id(entity_id)
+ end
+
+ def self.by_author_id(author_id)
+ new.by_author_id(author_id)
+ end
+
+ def self.by_entity_username(username)
+ new.by_entity_username(username)
+ end
+
+ def self.by_author_username(username)
+ new.by_author_username(username)
+ end
+
+ def self.find_user_id(username)
+ ::User.find_by_username(username)&.id
+ end
+ end
+ end
+end
diff --git a/lib/click_house/models/base_model.rb b/lib/click_house/models/base_model.rb
new file mode 100644
index 00000000000..89624076f15
--- /dev/null
+++ b/lib/click_house/models/base_model.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# rubocop: disable CodeReuse/ActiveRecord
+module ClickHouse
+ module Models
+ class BaseModel
+ extend Forwardable
+
+ def_delegators :@query_builder, :to_sql
+
+ def initialize(query_builder = ClickHouse::QueryBuilder.new(self.class.table_name))
+ @query_builder = query_builder
+ end
+
+ def self.table_name
+ raise NotImplementedError, "Subclasses must define a `table_name` class method"
+ end
+
+ def where(conditions)
+ self.class.new(@query_builder.where(conditions))
+ end
+
+ def order(field, direction = :asc)
+ self.class.new(@query_builder.order(field, direction))
+ end
+
+ def limit(count)
+ self.class.new(@query_builder.limit(count))
+ end
+
+ def offset(count)
+ self.class.new(@query_builder.offset(count))
+ end
+
+ def select(...)
+ self.class.new(@query_builder.select(...))
+ end
+ end
+ end
+end
+# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index e2a1b8296f6..580ba2bdc0d 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -78,13 +78,9 @@ module ContainerRegistry
delete_if_exists("/v2/#{name}/manifests/#{reference}")
end
- def delete_repository_tag_by_name(name, reference)
- delete_if_exists("/v2/#{name}/tags/reference/#{reference}")
- end
-
# Check if the registry supports tag deletion. This is only supported by the
# GitLab registry fork. The fastest and safest way to check this is to send
- # an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random
+ # an OPTIONS request to /v2/<name>/manifests/<tag>, using a random
# repository name and tag (the registry won't check if they exist).
# Registries that support tag deletion will reply with a 200 OK and include
# the DELETE method in the Allow header. Others reply with an 404 Not Found.
@@ -93,7 +89,7 @@ module ContainerRegistry
registry_features = Gitlab::CurrentSettings.container_registry_features || []
next true if ::Gitlab.com_except_jh? && registry_features.include?(REGISTRY_TAG_DELETE_FEATURE)
- response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {})
+ response = faraday.run_request(:options, '/v2/name/manifests/tag', '', {})
response.success? && response.headers['allow']&.include?('DELETE')
end
end
diff --git a/lib/container_registry/gitlab_api_client.rb b/lib/container_registry/gitlab_api_client.rb
index bd833ec00af..9b6c37da847 100644
--- a/lib/container_registry/gitlab_api_client.rb
+++ b/lib/container_registry/gitlab_api_client.rb
@@ -103,7 +103,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#compliance-check
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#compliance-check
def supports_gitlab_api?
strong_memoize(:supports_gitlab_api) do
registry_features = Gitlab::CurrentSettings.container_registry_features || []
@@ -116,19 +116,19 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def pre_import_repository(path)
response = start_import_for(path, pre: true)
IMPORT_RESPONSES.fetch(response.status, :error)
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_repository(path)
response = start_import_for(path, pre: false)
IMPORT_RESPONSES.fetch(response.status, :error)
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#cancel-repository-import
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def cancel_repository_import(path, force: false)
response = with_import_token_faraday do |faraday_client|
faraday_client.delete(import_url_for(path)) do |req|
@@ -142,7 +142,7 @@ module ContainerRegistry
{ status: status, migration_state: actual_state }
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#get-repository-import-status
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_status(path)
with_import_token_faraday do |faraday_client|
response = faraday_client.get(import_url_for(path))
@@ -156,7 +156,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#get-repository-details
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#get-repository-details
def repository_details(path, sizing: nil)
with_token_faraday do |faraday_client|
req = faraday_client.get("#{GITLAB_REPOSITORIES_PATH}/#{path}/") do |req|
@@ -169,7 +169,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#list-repository-tags
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-repository-tags
def tags(path, page_size: 100, last: nil, before: nil, name: nil, sort: nil)
limited_page_size = [page_size, MAX_TAGS_PAGE_SIZE].min
with_token_faraday do |faraday_client|
@@ -178,7 +178,7 @@ module ContainerRegistry
req.params['n'] = limited_page_size
req.params['last'] = last if last
req.params['before'] = before if before
- req.params['name'] = name if name
+ req.params['name'] = name if name.present?
req.params['sort'] = sort if sort
end
@@ -202,7 +202,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#list-sub-repositories
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-sub-repositories
def sub_repositories_with_tag(path, page_size: 100, last: nil)
limited_page_size = [page_size, MAX_REPOSITORIES_PAGE_SIZE].min
@@ -235,7 +235,7 @@ module ContainerRegistry
# Given a path 'group/subgroup/project' and name 'newname',
# with a successful rename, it will be 'group/subgroup/newname'
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#rename-base-repository
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#rename-base-repository
def rename_base_repository_path(path, name:, dry_run: false)
with_token_faraday do |faraday_client|
url = "#{GITLAB_REPOSITORIES_PATH}/#{path}/"
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index bf44b74cf7b..70742e8bd38 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -5,16 +5,25 @@ module ContainerRegistry
include Gitlab::Utils::StrongMemoize
attr_reader :repository, :name, :updated_at
- attr_writer :created_at
+ attr_writer :created_at, :manifest_digest, :revision, :total_size
delegate :registry, :client, to: :repository
- delegate :revision, :short_revision, to: :config_blob, allow_nil: true
def initialize(repository, name)
@repository = repository
@name = name
end
+ def revision
+ @revision || config_blob&.revision
+ end
+
+ def short_revision
+ return unless revision
+
+ revision[0..8]
+ end
+
def valid?
manifest.present?
end
@@ -53,7 +62,7 @@ module ContainerRegistry
def digest
strong_memoize(:digest) do
- client.repository_tag_digest(repository.path, name)
+ @manifest_digest || client.repository_tag_digest(repository.path, name)
end
end
@@ -126,6 +135,8 @@ module ContainerRegistry
# rubocop: disable CodeReuse/ActiveRecord
def total_size
+ return @total_size if @total_size
+
return unless layers
layers.sum(&:size) if v2?
diff --git a/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template b/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
index 886a3bd3116..df4c5382749 100644
--- a/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
+++ b/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
@@ -6,6 +6,8 @@
# Update below commented lines with appropriate values.
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
MIGRATION = "<%= class_name %>"
# DELAY_INTERVAL = 2.minutes
# BATCH_SIZE = <%= Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers::BATCH_SIZE %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
index f2f9acea923..a9cf5d085d1 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
SOURCE_TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
index 4896d931333..4ca4dd3c842 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
INDEX_NAME = :index_<%= source_table_name -%>_on_<%= partitioning_column -%>_<%= foreign_key_column %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
index b4e881074ad..16bd2548f18 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
SOURCE_TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
index bad7d17a51b..b065f390863 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/snowplow_event_definition_generator.rb b/lib/generators/gitlab/snowplow_event_definition_generator.rb
deleted file mode 100644
index b1a31541350..00000000000
--- a/lib/generators/gitlab/snowplow_event_definition_generator.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails/generators'
-
-module Gitlab
- class SnowplowEventDefinitionGenerator < Rails::Generators::Base
- CE_DIR = 'config/events'
- EE_DIR = 'ee/config/events'
-
- source_root File.expand_path('../../../generator_templates/snowplow_event_definition', __dir__)
-
- desc 'Generates an event definition yml file'
-
- class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if event is for ee'
- class_option :category, type: :string, optional: false, desc: 'Category of the event'
- class_option :action, type: :string, optional: false, desc: 'Action of the event'
-
- def create_event_file
- raise "Event definition already exists at #{file_path}" if definition_exists?
-
- template "event_definition.yml", file_path, force: false
- end
-
- def distributions
- (ee? ? ['- ee'] : ['- ce', '- ee']).join("\n")
- end
-
- def event_category
- options[:category]
- end
-
- def event_action
- options[:action]
- end
-
- def milestone
- Gitlab::VERSION.match('(\d+\.\d+)').captures.first
- end
-
- def ee?
- options[:ee]
- end
-
- private
-
- def definition_exists?
- File.exist?(ce_file_path) || File.exist?(ee_file_path)
- end
-
- def file_path
- ee? ? ee_file_path : ce_file_path
- end
-
- def ce_file_path
- File.join(CE_DIR, file_name)
- end
-
- def ee_file_path
- File.join(EE_DIR, file_name)
- end
-
- # Example of file name
- # 20230227000018_project_management_issue_title_changed.yml
- def file_name
- name = remove_special_chars("#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_#{event_category}_#{event_action}")
- "#{name[0..95]}.yml" # max 100 chars, see https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/2030#note_679501200
- end
-
- def remove_special_chars(input)
- input.gsub("::", "__").gsub(/[^A-Za-z0-9_]/, '')
- end
- end
-end
diff --git a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
index 8cd03978f27..f8a05d3132f 100644
--- a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
@@ -1,11 +1,14 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
module UsageMetricDefinition
class RedisHllGenerator < Rails::Generators::Base
- desc 'Generates a metric definition .yml file with defaults for Redis HLL.'
+ desc '[DEPRECATED] Generates a metric definition .yml file with defaults for Redis HLL.'
argument :category, type: :string, desc: "Category name"
argument :events, type: :array, desc: "Unique event names", banner: 'event_one event_two event_three'
diff --git a/lib/generators/gitlab/usage_metric_definition_generator.rb b/lib/generators/gitlab/usage_metric_definition_generator.rb
index d57a6b0b724..c231697e22e 100644
--- a/lib/generators/gitlab/usage_metric_definition_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition_generator.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
@@ -30,7 +33,7 @@ module Gitlab
source_root File.expand_path('../../../generator_templates/usage_metric_definition', __dir__)
- desc 'Generates metric definitions yml files'
+ desc '[DEPRECATED] Generates metric definitions yml files'
class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if metric is for ee'
class_option :dir,
@@ -40,6 +43,13 @@ module Gitlab
argument :key_paths, type: :array, desc: 'Unique JSON key paths for the metrics'
def create_metric_file
+ say("This generator is DEPRECATED. Use Internal Events tracking framework instead.")
+ # rubocop: disable Gitlab/DocUrl -- link for developers, not users
+ say("https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html")
+ # rubocop: enable Gitlab/DocUrl
+ desc = ask("Would you like to continue anyway? y/N") || 'n'
+ return unless desc.casecmp('y') == 0
+
validate!
key_paths.each do |key_path|
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 0875b14f7d0..b98a0207567 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -16,6 +16,11 @@ module Gitlab
Gitlab::VersionInfo.parse(Gitlab::VERSION)
end
+ def self.current_milestone
+ v = version_info
+ "#{v.major}.#{v.minor}"
+ end
+
def self.pre_release?
VERSION.include?('pre')
end
diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb
index cea25ba2db4..0c4a0afa1d5 100644
--- a/lib/gitlab/analytics/cycle_analytics/request_params.rb
+++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb
@@ -203,7 +203,8 @@ module Gitlab
def validate_date_range
return if created_after.nil? || created_before.nil?
- if (created_before - created_after) > MAX_RANGE_DAYS
+ time_period = created_before.at_beginning_of_day - created_after.at_beginning_of_day
+ if time_period > MAX_RANGE_DAYS
errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days'))
end
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index bf3f5b61825..469927b8a53 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -55,6 +55,7 @@ module Gitlab
phone_verification_send_code: { threshold: 10, interval: 1.hour },
phone_verification_verify_code: { threshold: 10, interval: 10.minutes },
namespace_exists: { threshold: 20, interval: 1.minute },
+ update_namespace_name: { threshold: -> { application_settings.update_namespace_name_rate_limit }, interval: 1.hour },
fetch_google_ip_list: { threshold: 10, interval: 1.minute },
project_fork_sync: { threshold: 10, interval: 30.minutes },
ai_action: { threshold: 160, interval: 8.hours },
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index fc1f7a1583c..578cfb52714 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -60,10 +60,10 @@ module Gitlab
Gitlab.config.omniauth.enabled
end
- def find_for_git_client(login, password, project:, ip:)
- raise "Must provide an IP for rate limiting" if ip.nil?
+ def find_for_git_client(login, password, project:, request:)
+ raise "Must provide an IP for rate limiting" if request.ip.nil?
- rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip)
+ rate_limiter = Gitlab::Auth::IpRateLimiter.new(request.ip)
raise IpBlocked if !skip_rate_limit?(login: login) && rate_limiter.banned?
@@ -80,7 +80,7 @@ module Gitlab
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result::EMPTY
- rate_limit!(rate_limiter, success: result.success?, login: login)
+ rate_limit!(rate_limiter, success: result.success?, login: login, request: request)
look_to_limit_user(result.actor)
return result if result.success? || authenticate_using_internal_or_ldap_password?
@@ -142,7 +142,7 @@ module Gitlab
private
- def rate_limit!(rate_limiter, success:, login:)
+ def rate_limit!(rate_limiter, success:, login:, request:)
return if skip_rate_limit?(login: login)
if success
@@ -155,8 +155,18 @@ module Gitlab
# request from this IP if needed.
# This returns true when the failures are over the threshold and the IP
# is banned.
- Gitlab::AppLogger.info "IP #{rate_limiter.ip} failed to login " \
- "as #{login} but has been temporarily banned from Git auth"
+
+ message = "Rack_Attack: Git auth failures has exceeded the threshold. " \
+ "IP has been temporarily banned from Git auth."
+
+ Gitlab::AuthLogger.error(
+ message: message,
+ env: :blocklist,
+ remote_ip: request.ip,
+ request_method: request.request_method,
+ path: request.fullpath,
+ login: login
+ )
end
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 7524d8b9f85..e6c9f04eff5 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -8,6 +8,21 @@ module Gitlab
def enabled?
::AuthHelper.saml_providers.any?
end
+
+ def default_attribute_statements
+ defaults = OmniAuth::Strategies::SAML.default_options[:attribute_statements].to_hash.deep_symbolize_keys
+ defaults[:nickname] = %w[username nickname]
+ defaults[:name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ defaults[:name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/name'
+ defaults[:email] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ defaults[:email] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress'
+ defaults[:first_name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'
+ defaults[:first_name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname'
+ defaults[:last_name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'
+ defaults[:last_name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/surname'
+
+ defaults
+ end
end
DEFAULT_PROVIDER_NAME = 'saml'
diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb
index fbdfd105ee3..4b66aaf0e6a 100644
--- a/lib/gitlab/auth/two_factor_auth_verifier.rb
+++ b/lib/gitlab/auth/two_factor_auth_verifier.rb
@@ -36,7 +36,7 @@ module Gitlab
return false unless time
- two_factor_grace_period.hours.since(time) < Time.current
+ two_factor_grace_period.hours.since(time).past?
end
def allow_2fa_bypass_for_provider
diff --git a/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb b/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb
new file mode 100644
index 00000000000..04fd09f81f0
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This migration populates the new `packages_tags.project_id` column from joining with `packages_packages` table
+ class BackfillPackagesTagsProjectId < BatchedMigrationJob
+ operation_name :update_all # This is used as the key on collecting metrics
+ scope_to ->(relation) { relation.where(project_id: nil) }
+ feature_category :package_registry
+
+ def perform
+ each_sub_batch do |sub_batch|
+ joined = sub_batch
+ .joins('INNER JOIN packages_packages ON packages_tags.package_id = packages_packages.id')
+ .select('packages_tags.id, packages_packages.project_id')
+
+ ApplicationRecord.connection.execute <<~SQL
+ WITH joined_cte(packages_tag_id, project_id) AS MATERIALIZED (
+ #{joined.to_sql}
+ )
+ UPDATE packages_tags
+ SET project_id = joined_cte.project_id
+ FROM joined_cte
+ WHERE id = joined_cte.packages_tag_id
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/batched_migration_job.rb b/lib/gitlab/background_migration/batched_migration_job.rb
index 952e6d01f1a..9e9fc9b98b7 100644
--- a/lib/gitlab/background_migration/batched_migration_job.rb
+++ b/lib/gitlab/background_migration/batched_migration_job.rb
@@ -130,7 +130,7 @@ module Gitlab
end
def base_relation
- define_batchable_model(batch_table, connection: connection)
+ define_batchable_model(batch_table, connection: connection, primary_key: batch_column)
.where(batch_column => start_id..end_id)
end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
new file mode 100644
index 00000000000..99bc638532a
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_merge_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchMergeAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_merge_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_merge_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
new file mode 100644
index 00000000000..a6934cf5adc
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_push_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchPushAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_push_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_push_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
new file mode 100644
index 00000000000..8c59e42a9f6
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_tag_create_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedTagCreateAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_tag_create_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_tags ON protected_tags.id = protected_tag_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_tag_create_access_levels.group_id
+ AND pgl.project_id = protected_tags.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
index 91994c2fa95..c8520993b8e 100644
--- a/lib/gitlab/base_doorkeeper_controller.rb
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -3,7 +3,8 @@
# This is a base controller for doorkeeper.
# It adds the `can?` helper used in the views.
module Gitlab
- class BaseDoorkeeperController < BaseActionController
+ # rubocop:disable Rails/ApplicationController
+ class BaseDoorkeeperController < ActionController::Base
include Gitlab::Allowable
include EnforcesTwoFactorAuthentication
include SessionsHelper
@@ -12,4 +13,5 @@ module Gitlab
helper_method :can?
end
+ # rubocop:enable Rails/ApplicationController
end
diff --git a/lib/gitlab/bitbucket_import/importers/issue_importer.rb b/lib/gitlab/bitbucket_import/importers/issue_importer.rb
index 2c3be67eabc..d194a311278 100644
--- a/lib/gitlab/bitbucket_import/importers/issue_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issue_importer.rb
@@ -40,6 +40,8 @@ module Gitlab
project.issues.create!(attributes)
+ metrics.issues_counter.increment
+
log_info(import_stage: 'import_issue', message: 'finished', iid: object[:iid])
rescue StandardError => e
track_import_failure!(project, exception: e)
diff --git a/lib/gitlab/bitbucket_import/importers/issues_importer.rb b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
index 6162433e701..8ab82ddb0be 100644
--- a/lib/gitlab/bitbucket_import/importers/issues_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
@@ -7,17 +7,21 @@ module Gitlab
include ParallelScheduling
def execute
+ return job_waiter unless repo.issues_enabled?
+
log_info(import_stage: 'import_issues', message: 'importing issues')
issues = client.issues(project.import_source)
labels = build_labels_hash
- issues.each do |issue|
+ issues.each_with_index do |issue, index|
job_waiter.jobs_remaining += 1
next if already_enqueued?(issue)
+ allocate_issues_internal_id! if index == 0
+
job_delay = calculate_job_delay(job_waiter.jobs_remaining)
issue_hash = issue.to_hash.merge({ issue_type_id: default_issue_type_id, label_id: labels[issue.kind] })
@@ -49,11 +53,23 @@ module Gitlab
::WorkItems::Type.default_issue_type.id
end
+ def allocate_issues_internal_id!
+ last_bitbucket_issue = client.last_issue(repo)
+
+ return unless last_bitbucket_issue
+
+ Issue.track_namespace_iid!(project.project_namespace, last_bitbucket_issue.iid)
+ end
+
def build_labels_hash
labels = {}
project.labels.each { |l| labels[l.title.to_s] = l.id }
labels
end
+
+ def repo
+ @repo ||= client.repo(project.import_source)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
index a18d50e8fce..f7b1753a9f9 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
@@ -45,6 +45,8 @@ module Gitlab
merge_request.assignee_ids = [author_id]
merge_request.reviewer_ids = reviewers
merge_request.save!
+
+ metrics.merge_requests_counter.increment
end
log_info(import_stage: 'import_pull_request', message: 'finished', iid: object[:iid])
diff --git a/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
index 8ea8b1562f2..934e4ee1720 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
@@ -4,21 +4,22 @@ module Gitlab
module BitbucketImport
module Importers
class PullRequestNotesImporter
- include Loggable
- include ErrorTracking
+ include ParallelScheduling
def initialize(project, hash)
@project = project
- @importer = Gitlab::BitbucketImport::Importer.new(project)
+ @formatter = Gitlab::ImportFormatter.new
+ @user_finder = UserFinder.new(project)
+ @ref_converter = Gitlab::BitbucketImport::RefConverter.new(project)
@object = hash.with_indifferent_access
+ @position_map = {}
+ @discussion_map = {}
end
def execute
log_info(import_stage: 'import_pull_request_notes', message: 'starting', iid: object[:iid])
- merge_request = project.merge_requests.find_by(iid: object[:iid]) # rubocop: disable CodeReuse/ActiveRecord
-
- importer.import_pull_request_comments(merge_request, merge_request) if merge_request
+ import_pull_request_comments if merge_request
log_info(import_stage: 'import_pull_request_notes', message: 'finished', iid: object[:iid])
rescue StandardError => e
@@ -27,7 +28,116 @@ module Gitlab
private
- attr_reader :object, :project, :importer
+ attr_reader :object, :project, :formatter, :user_finder, :ref_converter, :discussion_map, :position_map
+
+ def import_pull_request_comments
+ inline_comments, pr_comments = comments.partition(&:inline?)
+
+ import_inline_comments(inline_comments)
+ import_standalone_pr_comments(pr_comments)
+ end
+
+ def import_inline_comments(inline_comments)
+ children, parents = inline_comments.partition(&:has_parent?)
+
+ parents.each do |comment|
+ position_map[comment.iid] = build_position(comment)
+
+ import_comment(comment)
+ end
+
+ children.each do |comment|
+ position_map[comment.iid] = position_map.fetch(comment.parent_id, nil)
+
+ import_comment(comment)
+ end
+ end
+
+ def import_comment(comment)
+ position = position_map[comment.iid]
+ discussion_id = discussion_map[comment.parent_id]
+
+ note = create_diff_note(comment, position, discussion_id)
+
+ discussion_map[comment.iid] = note&.discussion_id
+ end
+
+ def create_diff_note(comment, position, discussion_id)
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(position: position, type: 'DiffNote', discussion_id: discussion_id)
+
+ note = merge_request.notes.build(attributes)
+
+ return note if note.save
+
+ # Bitbucket supports the ability to comment on any line, not just the
+ # line in the diff. If we can't add the note as a DiffNote, fallback to creating
+ # a regular note.
+
+ log_info(import_stage: 'create_diff_note', message: 'creating fallback DiffNote', iid: merge_request.iid)
+ create_fallback_diff_note(comment, position)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.log_exception(
+ e,
+ import_stage: 'create_diff_note', comment_id: comment.iid, error: e.message
+ )
+
+ nil
+ end
+
+ def create_fallback_diff_note(comment, position)
+ attributes = pull_request_comment_attributes(comment)
+ note = "*Comment on"
+
+ note += " #{position.old_path}:#{position.old_line} -->" if position&.old_line
+ note += " #{position.new_path}:#{position.new_line}" if position&.new_line
+ note += "*\n\n#{comment.note}"
+
+ attributes[:note] = note
+ merge_request.notes.create!(attributes)
+ end
+
+ def build_position(pr_comment)
+ params = {
+ diff_refs: merge_request.diff_refs,
+ old_path: pr_comment.file_path,
+ new_path: pr_comment.file_path,
+ old_line: pr_comment.old_pos,
+ new_line: pr_comment.new_pos
+ }
+
+ Gitlab::Diff::Position.new(params)
+ end
+
+ def import_standalone_pr_comments(pr_comments)
+ pr_comments.each do |comment|
+ attributes = pull_request_comment_attributes(comment)
+ merge_request.notes.create!(attributes)
+ end
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ author_id: user_finder.gitlab_user_id(project, comment.author),
+ note: comment_note(comment),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
+ end
+
+ def comment_note(comment)
+ author = formatter.author_line(comment.author) unless user_finder.find_user_id(comment.author)
+ author.to_s + ref_converter.convert_note(comment.note.to_s)
+ end
+
+ def merge_request
+ @merge_request ||= project.merge_requests.iid_in(object[:iid]).first
+ end
+
+ def comments
+ client.pull_request_comments(project.import_source, merge_request.iid).reject(&:deleted?)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/importers/repository_importer.rb b/lib/gitlab/bitbucket_import/importers/repository_importer.rb
index b8c0ba69d37..9be7ed99436 100644
--- a/lib/gitlab/bitbucket_import/importers/repository_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/repository_importer.rb
@@ -19,6 +19,7 @@ module Gitlab
validate_repository_size!
+ set_default_branch
update_clone_time
end
@@ -76,6 +77,16 @@ module Gitlab
def validate_repository_size!
# Defined in EE
end
+
+ def set_default_branch
+ default_branch = client.repo(project.import_source).default_branch
+
+ project.change_head(default_branch) if default_branch
+ end
+
+ def client
+ Bitbucket::Client.new(project.import_data.credentials)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/loggable.rb b/lib/gitlab/bitbucket_import/loggable.rb
index eda3cc96d4d..aeae993b9eb 100644
--- a/lib/gitlab/bitbucket_import/loggable.rb
+++ b/lib/gitlab/bitbucket_import/loggable.rb
@@ -19,6 +19,10 @@ module Gitlab
logger.error(log_data(messages))
end
+ def metrics
+ Gitlab::Import::Metrics.new(:bitbucket_importer, project)
+ end
+
private
def logger
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index 15b38188f13..a359236e150 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -31,8 +31,7 @@ module Gitlab
def treeish_objects
objects = commits
- return objects unless project.repository.empty? &&
- Feature.enabled?(:verify_push_rules_for_first_commit, project)
+ return objects unless project.repository.empty?
# It's a special case for the push to the empty repository
#
diff --git a/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb b/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
index 35f969dbb46..b8c6bdee1bb 100644
--- a/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
+++ b/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
@@ -6,7 +6,7 @@ module Gitlab
class AnyOversizedBlobs
def initialize(project:, changes:, file_size_limit_megabytes:)
@project = project
- @newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord just plucking from an array
+ @newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
@file_size_limit_megabytes = file_size_limit_megabytes
end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 21fc2980cdc..791b8a963e9 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -35,13 +35,15 @@ module Gitlab
end
attr_reader :offset, :sections, :segments, :current_segment,
- :section_header, :section_duration, :section_options
+ :section_header, :section_footer, :section_duration,
+ :section_options
def initialize(offset:, style:, sections: [])
@offset = offset
@segments = []
@sections = sections
@section_header = false
+ @section_footer = false
@duration = nil
@current_segment = Segment.new(style: style)
end
@@ -79,6 +81,10 @@ module Gitlab
@section_header = true
end
+ def set_as_section_footer
+ @section_footer = true
+ end
+
def set_section_duration(duration_in_seconds)
normalized_duration_in_seconds = duration_in_seconds.to_i.clamp(0, 1.year)
duration = ActiveSupport::Duration.build(normalized_duration_in_seconds)
@@ -103,6 +109,7 @@ module Gitlab
{ offset: offset, content: @segments }.tap do |result|
result[:section] = sections.last if sections.any?
result[:section_header] = true if @section_header
+ result[:section_footer] = true if @section_footer
result[:section_duration] = @section_duration if @section_duration
result[:section_options] = @section_options if @section_options
end
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index 3aec1cde1bc..6cf76fbbb51 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -49,6 +49,7 @@ module Gitlab
duration = timestamp.to_i - @open_sections[section].to_i
@current_line.set_section_duration(duration)
+ @current_line.set_as_section_footer
@open_sections.delete(section)
end
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
index 48b138b0258..bbcdcd7d389 100644
--- a/lib/gitlab/ci/build/context/build.rb
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -33,13 +33,9 @@ module Gitlab
# Assigning tags and needs is slow and they are not needed for rules
# evaluation since we don't use them to compute the variables at this point.
def build_attributes
- if pipeline.reduced_build_attributes_list_for_rules?
- attributes
- .except(:tag_list, :needs_attributes)
- .merge!(pipeline_attributes, ci_stage_attributes)
- else
- attributes.merge(pipeline_attributes, ci_stage_attributes)
- end
+ attributes
+ .except(:tag_list, :needs_attributes)
+ .merge!(pipeline_attributes, ci_stage_attributes)
end
def ci_stage_attributes
diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb
index df2b2a14fc6..50731d54fc0 100644
--- a/lib/gitlab/ci/components/instance_path.rb
+++ b/lib/gitlab/ci/components/instance_path.rb
@@ -18,7 +18,6 @@ module Gitlab
def initialize(address:)
@full_path, @version = address.to_s.split('@', 2)
@host = Settings.gitlab_ci['component_fqdn']
- @component_project = ::Ci::Catalog::ComponentsProject.new(project, sha)
end
def fetch_content!(current_user:)
@@ -27,7 +26,8 @@ module Gitlab
raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project)
- @component_project.fetch_component(component_name)
+ component_project = ::Ci::Catalog::ComponentsProject.new(project, sha)
+ component_project.fetch_component(component_name)
end
def project
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index bf8a99ef45e..5fcafcba829 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script image services start_in artifacts
cache dependencies before_script after_script hooks
coverage retry parallel interruptible timeout
- release id_tokens publish].freeze
+ release id_tokens publish pages].freeze
validations do
validates :config, allowed_keys: Gitlab::Ci::Config::Entry::Job.allowed_keys + PROCESSABLE_ALLOWED_KEYS
@@ -40,13 +40,19 @@ module Gitlab
if needs_value[:job].nil? && needs_value[:cross_dependency].present?
errors.add(:needs, "corresponding to dependencies must be from the same pipeline")
else
- missing_needs = dependencies - needs_value[:job].pluck(:name) # rubocop:disable CodeReuse/ActiveRecord (Array#pluck)
+ missing_needs = dependencies - needs_value[:job].pluck(:name) # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs") if missing_needs.any?
end
end
- validates :publish, absence: { message: "can only be used within a `pages` job" }, unless: -> { pages_job? }
+ validates :publish,
+ absence: { message: "can only be used within a `pages` job" },
+ unless: -> { pages_job? }
+
+ validates :pages,
+ absence: { message: "can only be used within a `pages` job" },
+ unless: -> { pages_job? }
end
entry :before_script, Entry::Commands,
@@ -127,10 +133,14 @@ module Gitlab
description: 'Path to be published with Pages',
inherit: false
+ entry :pages, ::Gitlab::Ci::Config::Entry::Pages,
+ inherit: false,
+ description: 'Pages configuration.'
+
attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in,
- :interruptible, :timeout,
- :release, :allow_failure, :publish
+ :interruptible, :timeout, :release,
+ :allow_failure, :publish, :pages
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -170,7 +180,8 @@ module Gitlab
needs: needs_defined? ? needs_value : nil,
scheduling_type: needs_defined? ? :dag : :stage,
id_tokens: id_tokens_value,
- publish: publish
+ publish: publish,
+ pages: pages
).compact
end
diff --git a/lib/gitlab/ci/config/entry/pages.rb b/lib/gitlab/ci/config/entry/pages.rb
new file mode 100644
index 00000000000..57d9e944f51
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/pages.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents the pages path prefix
+ # Entry that represents the pages attributes
+ #
+ class Pages < ::Gitlab::Config::Entry::Node
+ ALLOWED_KEYS = %i[path_prefix].freeze
+
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Validatable
+
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, type: Hash
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :path_prefix, type: String
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index 88734ac1186..d0e9a9afc51 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -25,6 +25,8 @@ module Gitlab
validates :name, type: Symbol
validates :name, length: { maximum: 255 }
+ validates :config, mutually_exclusive_keys: %i[script trigger]
+
validates :config, disallowed_keys: {
in: %i[only except start_in],
message: 'key may not be used with `rules`',
diff --git a/lib/gitlab/ci/config/header/input.rb b/lib/gitlab/ci/config/header/input.rb
index dcb96006459..08ee70b6290 100644
--- a/lib/gitlab/ci/config/header/input.rb
+++ b/lib/gitlab/ci/config/header/input.rb
@@ -11,17 +11,24 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[default description regex type].freeze
+ ALLOWED_KEYS = %i[default description options regex type].freeze
+ ALLOWED_OPTIONS_LIMIT = 50
attributes ALLOWED_KEYS, prefix: :input
validations do
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
validates :key, alphanumeric: true
- validates :input_default, alphanumeric: true, allow_nil: true
validates :input_description, alphanumeric: true, allow_nil: true
validates :input_regex, type: String, allow_nil: true
validates :input_type, allow_nil: true, allowed_values: Interpolation::Inputs.input_types
+ validates :input_options, type: Array, allow_nil: true
+
+ validate do
+ if input_options&.size.to_i > ALLOWED_OPTIONS_LIMIT
+ errors.add(:config, "cannot define more than #{ALLOWED_OPTIONS_LIMIT} options")
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/base_input.rb b/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
index ba519776635..987268b0525 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
@@ -20,11 +20,6 @@ module Gitlab
raise NotImplementedError
end
- # Checks whether the provided value is of the given type
- def valid_value?(value)
- raise NotImplementedError
- end
-
attr_reader :errors, :name, :spec, :value
def initialize(name:, spec:, value:)
@@ -54,20 +49,39 @@ module Gitlab
private
def validate!
- return error('required value has not been provided') if required_input? && value.nil?
+ validate_required
+
+ return if errors.present?
- # validate default value
- if !required_input? && !valid_value?(default)
- return error("default value is not a #{self.class.type_name}")
- end
+ run_validations(default, default: true) unless required_input?
- # validate provided value
- return error("provided value is not a #{self.class.type_name}") unless valid_value?(actual_value)
+ run_validations(value) unless value.nil?
+ end
+
+ def validate_required
+ error('required value has not been provided') if required_input? && value.nil?
+ end
- validate_regex!
+ def run_validations(value, default: false)
+ validate_type(value, default)
+ validate_options(value)
+ validate_regex(value, default)
end
- def validate_regex!
+ # Type validations are done separately for different input types.
+ def validate_type(_value, _default)
+ raise NotImplementedError
+ end
+
+ # Options can be either StringInput or NumberInput and are validated accordingly.
+ def validate_options(_value)
+ return unless options
+
+ error('Options can only be used with string and number inputs')
+ end
+
+ # Regex can be only be a StringInput and is validated accordingly.
+ def validate_regex(_value, _default)
return unless spec.key?(:regex)
error('RegEx validation can only be used with string inputs')
@@ -96,6 +110,10 @@ module Gitlab
def default
spec[:default]
end
+
+ def options
+ spec[:options]
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb b/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
index 0293c01a5a8..4c34f7e7fdd 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class BooleanInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
@@ -14,8 +16,11 @@ module Gitlab
'boolean'
end
- def valid_value?(value)
- [true, false].include?(value)
+ override :validate_type
+ def validate_type(value, default)
+ return if [true, false].include?(value)
+
+ error("#{default ? 'default' : 'provided'} value is not a boolean")
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/number_input.rb b/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
index 314315d2b6d..59bc057749a 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class NumberInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
@@ -14,8 +16,19 @@ module Gitlab
'number'
end
- def valid_value?(value)
- value.is_a?(Numeric)
+ override :validate_type
+ def validate_type(value, default)
+ return if value.is_a?(Numeric)
+
+ error("#{default ? 'default' : 'provided'} value is not a number")
+ end
+
+ override :validate_options
+ def validate_options(value)
+ return unless options && value
+ return if options.include?(value)
+
+ error("`#{value}` cannot be used because it is not in the list of the allowed options")
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/string_input.rb b/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
index 3f40e851f11..01b9d34a883 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class StringInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
# The input spec can be `nil` when using a minimal specification
# and also when `type` is not specified.
@@ -22,24 +24,32 @@ module Gitlab
'string'
end
- def valid_value?(value)
- value.nil? || value.is_a?(String)
+ override :validate_type
+ def validate_type(value, default)
+ return if value.is_a?(String)
+
+ error("#{default ? 'default' : 'provided'} value is not a string")
+ end
+
+ override :validate_options
+ def validate_options(value)
+ return unless options && value
+ return if options.include?(value)
+
+ error("`#{value}` cannot be used because it is not in the list of allowed options")
end
private
- def validate_regex!
+ override :validate_regex
+ def validate_regex(value, default)
return unless spec.key?(:regex)
safe_regex = ::Gitlab::UntrustedRegexp.new(spec[:regex])
- return if safe_regex.match?(actual_value)
+ return if safe_regex.match?(value)
- if value.nil?
- error('default value does not match required RegEx pattern')
- else
- error('provided value does not match required RegEx pattern')
- end
+ error("#{default ? 'default' : 'provided'} value does not match required RegEx pattern")
rescue RegexpError
error('invalid regular expression')
end
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index 29beba4774a..90db9d13d85 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -25,13 +25,26 @@ 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,
- user_identities: user_identities
+ aud: aud
}.compact)
end
+ def custom_claims
+ additional_custom_claims = {
+ runner_id: runner&.id,
+ runner_environment: runner_environment,
+ sha: pipeline.sha,
+ project_visibility: Gitlab::VisibilityLevel.string_level(project.visibility_level),
+ user_identities: user_identities
+ }.compact
+
+ mapper = ClaimMapper.new(project_config, pipeline)
+
+ super.merge(additional_custom_claims).merge(mapper.to_h)
+ end
+
def user_identities
return unless user&.pass_user_identities_to_ci_jwt
@@ -43,17 +56,6 @@ module Gitlab
end
end
- def custom_claims
- mapper = ClaimMapper.new(project_config, pipeline)
-
- super.merge({
- runner_id: runner&.id,
- runner_environment: runner_environment,
- sha: pipeline.sha,
- project_visibility: Gitlab::VisibilityLevel.string_level(project.visibility_level)
- }).merge(mapper.to_h)
- end
-
def project_config
Gitlab::Ci::ProjectConfig.new(
project: project,
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
index 3dc73544208..35548358c57 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
@@ -15,7 +15,8 @@ module Gitlab
SUPPORTED_SCHEMA_VERSION = '1'
GITLAB_PREFIX = 'gitlab:'
SOURCE_PARSERS = {
- 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning
+ 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning,
+ 'container_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning
}.freeze
SUPPORTED_PROPERTIES = %w[
meta:schema_version
@@ -24,6 +25,10 @@ module Gitlab
dependency_scanning:source_file:path
dependency_scanning:package_manager:name
dependency_scanning:language:name
+ container_scanning:image:name
+ container_scanning:image:tag
+ container_scanning:operating_system:name
+ container_scanning:operating_system:version
].freeze
def self.parse_source(...)
diff --git a/lib/gitlab/ci/parsers/sbom/source/base_source.rb b/lib/gitlab/ci/parsers/sbom/source/base_source.rb
new file mode 100644
index 00000000000..744555aa25a
--- /dev/null
+++ b/lib/gitlab/ci/parsers/sbom/source/base_source.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Sbom
+ module Source
+ class BaseSource
+ REQUIRED_ATTRIBUTES = [].freeze
+
+ def self.source(...)
+ new(...).source
+ end
+
+ def initialize(data)
+ @data = data
+ end
+
+ def source
+ return unless required_attributes_present?
+
+ ::Gitlab::Ci::Reports::Sbom::Source.new(
+ type: type,
+ data: data
+ )
+ end
+
+ private
+
+ attr_reader :data
+
+ # Implement in child class
+ # returns a symbol of the source type
+ def type; end
+
+ def required_attributes_present?
+ self.class::REQUIRED_ATTRIBUTES.all? do |keys|
+ data.dig(*keys).present?
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
new file mode 100644
index 00000000000..33f9631c424
--- /dev/null
+++ b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Sbom
+ module Source
+ class ContainerScanning < BaseSource
+ REQUIRED_ATTRIBUTES = [
+ %w[image name],
+ %w[image tag]
+ ].freeze
+
+ OPERATING_SYSTEM_ATTRIBUTES = [
+ %w[operating_system name],
+ %w[operating_system version]
+ ].freeze
+
+ private
+
+ def type
+ :container_scanning
+ end
+
+ def required_attributes_present?
+ operating_system_attributes_valid? && super
+ end
+
+ def operating_system_attributes_valid?
+ return true if data['operating_system'].blank?
+
+ OPERATING_SYSTEM_ATTRIBUTES.all? do |keys|
+ data.dig(*keys).present?
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
index c76a4309779..fc5a7606e39 100644
--- a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
+++ b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
@@ -5,36 +5,15 @@ module Gitlab
module Parsers
module Sbom
module Source
- class DependencyScanning
+ class DependencyScanning < BaseSource
REQUIRED_ATTRIBUTES = [
%w[input_file path]
].freeze
- def self.source(...)
- new(...).source
- end
-
- def initialize(data)
- @data = data
- end
-
- def source
- return unless required_attributes_present?
-
- ::Gitlab::Ci::Reports::Sbom::Source.new(
- type: :dependency_scanning,
- data: data
- )
- end
-
private
- attr_reader :data
-
- def required_attributes_present?
- REQUIRED_ATTRIBUTES.all? do |keys|
- data.dig(*keys).present?
- end
+ def type
+ :dependency_scanning
end
end
end
diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb
index 9032faa66d4..be6c6c2558b 100644
--- a/lib/gitlab/ci/parsers/security/common.rb
+++ b/lib/gitlab/ci/parsers/security/common.rb
@@ -141,7 +141,7 @@ module Gitlab
project_id: @project.id,
found_by_pipeline: report.pipeline,
vulnerability_finding_signatures_enabled: @signatures_enabled,
- cvss: data['cvss'] || []
+ cvss: data['cvss_vectors'] || []
)
)
end
diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
index e39482481c7..e2a8044b708 100644
--- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
+++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
@@ -7,14 +7,14 @@ module Gitlab
module Validators
class SchemaValidator
SUPPORTED_VERSIONS = {
- cluster_image_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- container_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- coverage_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- dast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- api_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- dependency_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- sast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- secret_detection: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6]
+ cluster_image_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ container_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ coverage_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ dast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ api_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ dependency_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ sast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ secret_detection: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7]
}.freeze
VERSIONS_TO_REMOVE_IN_17_0 = %w[].freeze
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json
new file mode 100644
index 00000000000..e27096d071f
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json
@@ -0,0 +1,1085 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/cluster-image-scanning-report-format.json",
+ "title": "Report format for GitLab Cluster Image Scanning",
+ "description": "This schema provides the the report format for Cluster Image Scanning (https://docs.gitlab.com/ee/user/application_security/cluster_image_scanning/).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "cluster_image_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "image",
+ "kubernetes_resource"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image.",
+ "examples": [
+ "index.docker.io/library/nginx:1.21"
+ ]
+ },
+ "kubernetes_resource": {
+ "type": "object",
+ "description": "The specific Kubernetes resource that was scanned.",
+ "required": [
+ "namespace",
+ "kind",
+ "name",
+ "container_name"
+ ],
+ "properties": {
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes namespace the resource that had its image scanned.",
+ "examples": [
+ "default",
+ "staging",
+ "production"
+ ]
+ },
+ "kind": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes kind the resource that had its image scanned.",
+ "examples": [
+ "Deployment",
+ "DaemonSet"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the resource that had its image scanned.",
+ "examples": [
+ "nginx-ingress"
+ ]
+ },
+ "container_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the container that had its image scanned.",
+ "examples": [
+ "nginx"
+ ]
+ },
+ "agent_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes Agent which performed the scan.",
+ "examples": [
+ "1234"
+ ]
+ },
+ "cluster_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes cluster when using cluster integration.",
+ "examples": [
+ "1234"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json
new file mode 100644
index 00000000000..94c3b3fc919
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json
@@ -0,0 +1,1017 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/container-scanning-report-format.json",
+ "title": "Report format for GitLab Container Scanning",
+ "description": "This schema provides the the report format for Container Scanning (https://docs.gitlab.com/ee/user/application_security/container_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "container_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "operating_system",
+ "image"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image."
+ },
+ "default_branch_image": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the image on the default branch."
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json
new file mode 100644
index 00000000000..e15fbc3ed56
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json
@@ -0,0 +1,975 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/coverage-fuzzing-report-format.json",
+ "title": "Report format for GitLab Fuzz Testing",
+ "description": "This schema provides the report format for Coverage Guided Fuzz Testing (https://docs.gitlab.com/ee/user/application_security/coverage_fuzzing).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "coverage_fuzzing"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "description": "The location of the error",
+ "type": "object",
+ "properties": {
+ "crash_address": {
+ "type": "string",
+ "description": "The relative address in memory were the crash occurred.",
+ "examples": [
+ "0xabababab"
+ ]
+ },
+ "stacktrace_snippet": {
+ "type": "string",
+ "description": "The stack trace recorded during fuzzing resulting the crash.",
+ "examples": [
+ "func_a+0xabcd\nfunc_b+0xabcc"
+ ]
+ },
+ "crash_state": {
+ "type": "string",
+ "description": "Minimised and normalized crash stack-trace (called crash_state).",
+ "examples": [
+ "func_a+0xa\nfunc_b+0xb\nfunc_c+0xc"
+ ]
+ },
+ "crash_type": {
+ "type": "string",
+ "description": "Type of the crash.",
+ "examples": [
+ "Heap-Buffer-overflow",
+ "Division-by-zero"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json
new file mode 100644
index 00000000000..8a9519f442f
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json
@@ -0,0 +1,1380 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dast-report-format.json",
+ "title": "Report format for GitLab DAST",
+ "description": "This schema provides the the report format for Dynamic Application Security Testing (https://docs.gitlab.com/ee/user/application_security/dast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanned_resources",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dast",
+ "api_fuzzing"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "scanned_resources": {
+ "type": "array",
+ "description": "The attack surface scanned by DAST.",
+ "items": {
+ "type": "object",
+ "required": [
+ "method",
+ "url",
+ "type"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method of the scanned resource.",
+ "examples": [
+ "GET",
+ "POST",
+ "HEAD"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the scanned resource.",
+ "examples": [
+ "http://my.site.com/a-page"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Type of the scanned resource, for DAST, this must be 'url'.",
+ "examples": [
+ "url"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "evidence": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "object",
+ "description": "Source of evidence",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique source identifier",
+ "examples": [
+ "assert:LogAnalysis",
+ "assert:StatusCode"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Source display name",
+ "examples": [
+ "Log Analysis",
+ "Status Code"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "Link to additional information",
+ "examples": [
+ "https://docs.gitlab.com/ee/development/integrations/secure.html"
+ ]
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "description": "Human readable string containing evidence of the vulnerability.",
+ "examples": [
+ "Credit card 4111111111111111 found",
+ "Server leaked information nginx/1.17.6"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ },
+ "supporting_messages": {
+ "type": "array",
+ "description": "Array of supporting http messages.",
+ "items": {
+ "type": "object",
+ "description": "A supporting http message.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Message display name.",
+ "examples": [
+ "Unmodified",
+ "Recorded"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "hostname": {
+ "type": "string",
+ "description": "The protocol, domain, and port of the application where the vulnerability was found."
+ },
+ "method": {
+ "type": "string",
+ "description": "The HTTP method that was used to request the URL where the vulnerability was found."
+ },
+ "param": {
+ "type": "string",
+ "description": "A value provided by a vulnerability rule related to the found vulnerability. Examples include a header value, or a parameter used in a HTTP POST."
+ },
+ "path": {
+ "type": "string",
+ "description": "The path of the URL where the vulnerability was found. Typically, this would start with a forward slash."
+ }
+ }
+ },
+ "assets": {
+ "type": "array",
+ "description": "Array of build assets associated with vulnerability.",
+ "items": {
+ "type": "object",
+ "description": "Describes an asset associated with vulnerability.",
+ "required": [
+ "type",
+ "name",
+ "url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "The type of asset",
+ "enum": [
+ "http_session",
+ "postman"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Display name for asset",
+ "examples": [
+ "HTTP Messages",
+ "Postman Collection"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Link to asset in build artifacts",
+ "examples": [
+ "https://gitlab.com/gitlab-org/security-products/dast/-/jobs/626397001/artifacts/file//output/zap_session.data"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json
new file mode 100644
index 00000000000..83b3537b5f1
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json
@@ -0,0 +1,1083 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dependency-scanning-report-format.json",
+ "title": "Report format for GitLab Dependency Scanning",
+ "description": "This schema provides the the report format for Dependency Scanning analyzers (https://docs.gitlab.com/ee/user/application_security/dependency_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "dependency_files",
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dependency_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "file",
+ "dependency"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Path to the manifest or lock file where the dependency is declared (such as yarn.lock)."
+ },
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ },
+ "dependency_files": {
+ "type": "array",
+ "description": "List of dependency files identified in the project.",
+ "items": {
+ "type": "object",
+ "required": [
+ "path",
+ "package_manager",
+ "dependencies"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1
+ },
+ "package_manager": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json
new file mode 100644
index 00000000000..3597ed169d5
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json
@@ -0,0 +1,970 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/sast-report-format.json",
+ "title": "Report format for GitLab SAST",
+ "description": "This schema provides the report format for Static Application Security Testing analyzers (https://docs.gitlab.com/ee/user/application_security/sast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "sast"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability."
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located."
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located."
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json
new file mode 100644
index 00000000000..afd80ca916b
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json
@@ -0,0 +1,994 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/secret-detection-report-format.json",
+ "title": "Report format for GitLab Secret Detection",
+ "description": "This schema provides the the report format for the Secret Detection analyzer (https://docs.gitlab.com/ee/user/application_security/secret_detection)",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "secret_detection"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "required": [
+ "commit"
+ ],
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located"
+ },
+ "commit": {
+ "type": "object",
+ "description": "Represents the commit in which the vulnerability was detected",
+ "required": [
+ "sha"
+ ],
+ "properties": {
+ "author": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "sha": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability"
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability"
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located"
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located"
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 43fb5cdbbe6..b8c8cfa802c 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -6,7 +6,7 @@ module Gitlab
module Build
class Cancelable < Status::Extended
def has_action?
- can?(user, :update_build, subject)
+ can?(user, :cancel_build, subject)
end
def action_icon
diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb
index 1ba78b357e5..fe4f6db9549 100644
--- a/lib/gitlab/ci/status/composite.rb
+++ b/lib/gitlab/ci/status/composite.rb
@@ -61,6 +61,8 @@ module Gitlab
'running'
elsif any_of?(:waiting_for_resource)
'waiting_for_resource'
+ elsif any_of?(:waiting_for_callback)
+ 'waiting_for_callback'
elsif any_of?(:manual)
'manual'
elsif any_of?(:scheduled)
diff --git a/lib/gitlab/ci/status/waiting_for_callback.rb b/lib/gitlab/ci/status/waiting_for_callback.rb
new file mode 100644
index 00000000000..0184a910ede
--- /dev/null
+++ b/lib/gitlab/ci/status/waiting_for_callback.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ class WaitingForCallback < Status::Core
+ def text
+ s_('CiStatusText|Waiting')
+ end
+
+ def label
+ s_('CiStatusLabel|waiting for callback')
+ end
+
+ def icon
+ 'status_pending'
+ end
+
+ def favicon
+ 'favicon_status_pending'
+ end
+
+ def group
+ 'waiting-for-callback'
+ end
+
+ def details_path
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
index 356062c734e..324128678de 100644
--- a/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
@@ -12,9 +12,9 @@ include:
docker-build:
variables:
- COSIGN_YES: "true" # Used by Cosign to skip confirmation prompts for non-destructive operations
+ COSIGN_YES: "true" # Used by Cosign to skip confirmation prompts for non-destructive operations
id_tokens:
- SIGSTORE_ID_TOKEN: # Used by Cosign to get certificate from Fulcio
+ SIGSTORE_ID_TOKEN: # Used by Cosign to get certificate from Fulcio
aud: sigstore
after_script:
- apk add --update cosign
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 2d04c97b32e..6898923bc53 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.44.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.49.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index 2d04c97b32e..6898923bc53 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.44.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.49.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 4d53b92763a..7d923245d79 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 390824e8e49..0f8d5bf6d8f 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index a9681c0f927..e29d18ea45a 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
index d7a6104082d..4c89497fa97 100644
--- a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
@@ -46,13 +46,30 @@ kaniko-build:
# Write credentials to access Gitlab Container Registry within the runner/ci
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
# Build and push the container. To disable push add --no-push
- - DOCKERFILE_PATH=${DOCKERFILE_PATH:-"$KANIKO_BUILD_CONTEXT/Dockerfile"}
+ # Both Dockerfile and Containerfile are supported. For retrocompatibility, if both files are present, Dockerfile will be used.
+ - |
+ if [ -z "$DOCKERFILE_PATH" ]; then
+ if [ -f "$KANIKO_BUILD_CONTEXT/Dockerfile" ]; then
+ DOCKERFILE_PATH="$KANIKO_BUILD_CONTEXT/Dockerfile"
+ elif [ -n "$CONTAINERFILE_PATH" ]; then
+ DOCKERFILE_PATH="$CONTAINERFILE_PATH"
+ elif [ -f "$KANIKO_BUILD_CONTEXT/Containerfile" ]; then
+ DOCKERFILE_PATH="$KANIKO_BUILD_CONTEXT/Containerfile"
+ else \
+ echo "No suitable configuration for the build context have been found. Please check your configuration."
+ exit 1
+ fi
+ fi
+ - echo $DOCKERFILE_PATH
- /kaniko/executor --context $KANIKO_BUILD_CONTEXT --dockerfile $DOCKERFILE_PATH --destination $IMAGE_TAG $KANIKO_ARGS
- # Run this job in a branch/tag where a Dockerfile exists
+ # Run this job in a branch/tag where a Containerfile/Dockerfile exists
rules:
- exists:
+ - Containerfile
- Dockerfile
- # custom Dockerfile path
+ # custom Containerfile/Dockerfile path
+ # If both variables are set, DOCKERFILE_PATH will be used
- if: $DOCKERFILE_PATH
+ - if: $CONTAINERFILE_PATH
# custom build context without an explicit Dockerfile path
- if: $KANIKO_BUILD_CONTEXT != $CI_PROJECT_DIR
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
index d2b929cf995..0ba4f9715c5 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -50,7 +50,11 @@ variables:
- gitlab-terraform plan-json
resource_group: ${TF_STATE_NAME}
artifacts:
- # The next line, which disables public access to pipeline artifacts, may not be available everywhere.
+ # Terraform's cache files can include secrets which can be accidentally exposed.
+ # Please exercise caution when utilizing secrets in your Terraform infrastructure and
+ # consider limiting access to artifacts or take other security measures to protect sensitive information.
+ #
+ # The next line, which disables public access to pipeline artifacts, is not available on GitLab.com.
# See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
public: false
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
index c1a90955f7f..8c9e0a329dd 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: 26.1.0
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
index adc92fde5ae..3f4c0c53850 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: latest
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/yaml_processor/dag.rb b/lib/gitlab/ci/yaml_processor/dag.rb
index 4a122c73e80..d3047385c99 100644
--- a/lib/gitlab/ci/yaml_processor/dag.rb
+++ b/lib/gitlab/ci/yaml_processor/dag.rb
@@ -17,13 +17,15 @@ module Gitlab
def self.check_circular_dependencies!(jobs)
new(jobs).tsort
- rescue TSort::Cyclic
- raise ValidationError, 'The pipeline has circular dependencies'
+ rescue TSort::Cyclic => e
+ raise ValidationError, "The pipeline has circular dependencies: #{e.message}"
end
def tsort_each_child(node, &block)
return unless @nodes[node]
+ raise TSort::Cyclic, "self-dependency: #{node}" if @nodes[node].include?(node)
+
@nodes[node].each(&block)
end
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 6207b595fc6..2435d128bf2 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -8,8 +8,7 @@ module Gitlab
class Result
attr_reader :errors, :warnings,
:root_variables, :root_variables_with_prefill_data,
- :stages, :jobs,
- :workflow_rules, :workflow_name
+ :stages, :jobs, :workflow_rules, :workflow_name
def initialize(ci_config: nil, errors: [], warnings: [])
@ci_config = ci_config
@@ -124,7 +123,8 @@ module Gitlab
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: job[:release],
- publish: job[:publish]
+ publish: job[:publish],
+ pages: job[:pages]
}.compact }.compact
end
diff --git a/lib/gitlab/composer/version_index.rb b/lib/gitlab/composer/version_index.rb
index bfa3112b795..0834fda9cf9 100644
--- a/lib/gitlab/composer/version_index.rb
+++ b/lib/gitlab/composer/version_index.rb
@@ -48,8 +48,7 @@ module Gitlab
end
def package_source(package)
- use_http_url = package.project.public? || Feature.disabled?(:composer_use_ssh_source_urls, package.project)
- git_url = use_http_url ? package.project.http_url_to_repo : package.project.ssh_url_to_repo
+ git_url = package.project.public? ? package.project.http_url_to_repo : package.project.ssh_url_to_repo
{
'type' => 'git',
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 87b7cab3f6d..c7dd11b0432 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -56,8 +56,7 @@ module Gitlab
mutually_exclusive_keys = value.try(:keys).to_a & options[:in]
if mutually_exclusive_keys.length > 1
- record.errors.add(attribute, "please use only one of the following keys: " +
- mutually_exclusive_keys.join(', '))
+ record.errors.add(attribute, "these keys cannot be used together: #{mutually_exclusive_keys.join(', ')}")
end
end
end
diff --git a/lib/gitlab/config_checker/puma_rugged_checker.rb b/lib/gitlab/config_checker/puma_rugged_checker.rb
deleted file mode 100644
index 82c59f3328b..00000000000
--- a/lib/gitlab/config_checker/puma_rugged_checker.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ConfigChecker
- module PumaRuggedChecker
- extend self
- extend Gitlab::Git::RuggedImpl::UseRugged
-
- def check
- notices = []
-
- if running_puma_with_multiple_threads? && rugged_enabled_through_feature_flag?
- link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">'
- link_end = '</a>'
- notices << {
- type: 'warning',
- message: _('Puma is running with a thread count above 1 and the Rugged '\
- 'service is enabled. This may decrease performance in some environments. '\
- 'See our %{link_start}documentation%{link_end} '\
- 'for details of this issue.') % { link_start: link_start, link_end: link_end }
- }
- end
-
- notices
- end
- end
- end
-end
diff --git a/lib/gitlab/database/dictionary.rb b/lib/gitlab/database/dictionary.rb
new file mode 100644
index 00000000000..7b0c8560a26
--- /dev/null
+++ b/lib/gitlab/database/dictionary.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class Dictionary
+ def initialize(file_path)
+ @file_path = file_path
+ @data = YAML.load_file(file_path)
+ end
+
+ def name_and_schema
+ [key_name, gitlab_schema.to_sym]
+ end
+
+ def table_name
+ data['table_name']
+ end
+
+ def view_name
+ data['view_name']
+ end
+
+ def milestone
+ data['milestone']
+ end
+
+ def gitlab_schema
+ data['gitlab_schema']
+ end
+
+ def schema?(schema_name)
+ gitlab_schema == schema_name.to_s
+ end
+
+ def key_name
+ table_name || view_name
+ end
+
+ def validate!
+ return true unless gitlab_schema.nil?
+
+ raise(
+ GitlabSchema::UnknownSchemaError,
+ "#{file_path} must specify a valid gitlab_schema for #{key_name}. " \
+ "See #{help_page_url}"
+ )
+ end
+
+ private
+
+ attr_reader :file_path, :data
+
+ def help_page_url
+ # rubocop:disable Gitlab/DocUrl -- link directly to docs.gitlab.com, always
+ 'https://docs.gitlab.com/ee/development/database/database_dictionary.html'
+ # rubocop:enable Gitlab/DocUrl
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/dynamic_model_helpers.rb b/lib/gitlab/database/dynamic_model_helpers.rb
index 83edf77f37e..18854530278 100644
--- a/lib/gitlab/database/dynamic_model_helpers.rb
+++ b/lib/gitlab/database/dynamic_model_helpers.rb
@@ -5,7 +5,7 @@ module Gitlab
module DynamicModelHelpers
BATCH_SIZE = 1_000
- def define_batchable_model(table_name, connection:)
+ def define_batchable_model(table_name, connection:, primary_key: nil)
klass = Class.new(ActiveRecord::Base) do
include EachBatch
@@ -13,6 +13,7 @@ module Gitlab
self.inheritance_column = :_type_disabled
end
+ klass.primary_key = primary_key if connection.primary_keys(table_name).length > 1
klass.connection = connection
klass
end
diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb
index 31ceb898eee..ecb45622061 100644
--- a/lib/gitlab/database/gitlab_schema.rb
+++ b/lib/gitlab/database/gitlab_schema.rb
@@ -31,6 +31,7 @@ module Gitlab
'_test_gitlab_main_cell_' => :gitlab_main_cell,
'_test_gitlab_main_' => :gitlab_main,
'_test_gitlab_ci_' => :gitlab_ci,
+ '_test_gitlab_jh_' => :gitlab_jh,
'_test_gitlab_embedding_' => :gitlab_embedding,
'_test_gitlab_geo_' => :gitlab_geo,
'_test_gitlab_pm_' => :gitlab_pm,
@@ -138,19 +139,19 @@ module Gitlab
end
def self.deleted_tables_to_schema
- @deleted_tables_to_schema ||= self.build_dictionary('deleted_tables').to_h
+ @deleted_tables_to_schema ||= self.build_dictionary('deleted_tables').map(&:name_and_schema).to_h
end
def self.deleted_views_to_schema
- @deleted_views_to_schema ||= self.build_dictionary('deleted_views').to_h
+ @deleted_views_to_schema ||= self.build_dictionary('deleted_views').map(&:name_and_schema).to_h
end
def self.tables_to_schema
- @tables_to_schema ||= self.build_dictionary('').to_h
+ @tables_to_schema ||= self.build_dictionary('').map(&:name_and_schema).to_h
end
def self.views_to_schema
- @views_to_schema ||= self.build_dictionary('views').to_h
+ @views_to_schema ||= self.build_dictionary('views').map(&:name_and_schema).to_h
end
def self.schema_names
@@ -159,21 +160,9 @@ module Gitlab
def self.build_dictionary(scope)
Dir.glob(dictionary_path_globs(scope)).map do |file_path|
- data = YAML.load_file(file_path)
-
- key_name = data['table_name'] || data['view_name']
-
- # rubocop:disable Gitlab/DocUrl
- if data['gitlab_schema'].nil?
- raise(
- UnknownSchemaError,
- "#{file_path} must specify a valid gitlab_schema for #{key_name}. " \
- "See https://docs.gitlab.com/ee/development/database/database_dictionary.html"
- )
- end
- # rubocop:enable Gitlab/DocUrl
-
- [key_name, data['gitlab_schema'].to_sym]
+ dictionary = Dictionary.new(file_path)
+ dictionary.validate!
+ dictionary
end
end
end
diff --git a/lib/gitlab/database/migration.rb b/lib/gitlab/database/migration.rb
index 41044816de9..1d17c2ca608 100644
--- a/lib/gitlab/database/migration.rb
+++ b/lib/gitlab/database/migration.rb
@@ -56,6 +56,10 @@ module Gitlab
include Gitlab::Database::Migrations::RunnerBackoff::MigrationHelpers
end
+ class V2_2 < V2_1
+ include Gitlab::Database::Migrations::MilestoneMixin
+ end
+
def self.[](version)
version = version.to_s
name = "V#{version.tr('.', '_')}"
@@ -66,7 +70,7 @@ module Gitlab
# The current version to be used in new migrations
def self.current_version
- 2.1
+ 2.2
end
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index efcceafda90..a57bce789c7 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -18,8 +18,8 @@ module Gitlab
include AsyncConstraints::MigrationHelpers
include WraparoundVacuumHelpers
- def define_batchable_model(table_name, connection: self.connection)
- super(table_name, connection: connection)
+ def define_batchable_model(table_name, connection: self.connection, primary_key: nil)
+ super(table_name, connection: connection, primary_key: primary_key)
end
def each_batch(table_name, connection: self.connection, **kwargs)
@@ -821,6 +821,7 @@ module Gitlab
primary_key: :id,
batch_size: 20_000,
sub_batch_size: 1000,
+ pause_ms: 100,
interval: 2.minutes
)
@@ -848,6 +849,7 @@ module Gitlab
conversions.keys,
conversions.values,
job_interval: interval,
+ pause_ms: pause_ms,
batch_size: batch_size,
sub_batch_size: sub_batch_size)
end
diff --git a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
index 11f1e62e8b9..d1edb739b85 100644
--- a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
+++ b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
@@ -4,12 +4,16 @@ module Gitlab
module Database
module MigrationHelpers
module ConvertToBigint
- # This helper is extracted for the purpose of
- # https://gitlab.com/gitlab-org/gitlab/-/issues/392815
- # so that we can test all combinations just once,
- # and simplify migration tests.
- #
- # Once we are done with the PK conversions we can remove this.
+ INDEX_OPTIONS_MAP = {
+ unique: :unique,
+ order: :orders,
+ opclass: :opclasses,
+ where: :where,
+ type: :type,
+ using: :using,
+ comment: :comment
+ }.freeze
+
def com_or_dev_or_test_but_not_jh?
return true if Gitlab.dev_or_test_env?
@@ -29,6 +33,78 @@ module Gitlab
column.sql_type == 'bigint' && temp_column.sql_type == 'integer'
end
+
+ def add_bigint_column_indexes(table_name, int_column_name)
+ bigint_column_name = convert_to_bigint_column(int_column_name)
+
+ unless column_exists?(table_name.to_s, bigint_column_name)
+ raise "Bigint column '#{bigint_column_name}' does not exist on #{table_name}"
+ end
+
+ indexes(table_name).each do |i|
+ next unless Array(i.columns).join(' ').match?(/\b#{int_column_name}\b/)
+
+ create_bigint_index(table_name, i, int_column_name, bigint_column_name)
+ end
+ end
+
+ # default 'index_name' method is not used because this method can be reused while swapping/dropping the indexes
+ def bigint_index_name(int_column_index_name)
+ # First 20 digits of the hash is chosen to make sure it fits the 63 chars limit
+ digest = Digest::SHA256.hexdigest(int_column_index_name).first(20)
+ "bigint_idx_#{digest}"
+ end
+
+ private
+
+ def create_bigint_index(table_name, index_definition, int_column_name, bigint_column_name)
+ index_attributes = index_definition.as_json
+ index_options = INDEX_OPTIONS_MAP
+ .transform_values { |key| index_attributes[key.to_s] }
+ .select { |_, v| v.present? }
+
+ bigint_index_options = create_bigint_options(
+ index_options,
+ index_definition.name,
+ int_column_name,
+ bigint_column_name
+ )
+
+ add_concurrent_index(
+ table_name,
+ bigint_index_columns(int_column_name, bigint_column_name, index_definition.columns),
+ name: bigint_index_options.delete(:name),
+ ** bigint_index_options
+ )
+ end
+
+ def bigint_index_columns(int_column_name, bigint_column_name, int_index_columns)
+ if int_index_columns.is_a?(String)
+ int_index_columns.gsub(/\b#{int_column_name}\b/, bigint_column_name)
+ else
+ int_index_columns.map do |column|
+ column == int_column_name.to_s ? bigint_column_name : column
+ end
+ end
+ end
+
+ def create_bigint_options(index_options, int_index_name, int_column_name, bigint_column_name)
+ index_options[:name] = bigint_index_name(int_index_name)
+ index_options[:where]&.gsub!(/\b#{int_column_name}\b/, bigint_column_name)
+
+ # ordering on multiple columns will return a Hash instead of string
+ index_options[:order] =
+ if index_options[:order].is_a?(Hash)
+ index_options[:order].to_h do |column, order|
+ column = bigint_column_name if column == int_column_name
+ [column, order]
+ end
+ else
+ index_options[:order]&.gsub(/\b#{int_column_name}\b/, bigint_column_name)
+ end
+
+ index_options.select { |_, v| v.present? }
+ end
end
end
end
diff --git a/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
index 555efb58606..7f215bc0db7 100644
--- a/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
+++ b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
@@ -13,7 +13,7 @@ module Gitlab
# 3. Introduce the migration again for self-managed.
#
def can_execute_on?(*tables)
- return false unless Gitlab.com? || Gitlab.dev_or_test_env?
+ return false unless Gitlab.com_except_jh? || Gitlab.dev_or_test_env?
if wraparound_prevention_on_tables?(tables)
Gitlab::AppLogger.info(message: "Wraparound prevention vacuum detected", class: self.class)
diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
index 64cde273a59..3d4ac113bf6 100644
--- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
@@ -72,6 +72,7 @@ module Gitlab
batch_max_value: nil,
batch_class_name: BATCH_CLASS_NAME,
batch_size: BATCH_SIZE,
+ pause_ms: 100,
max_batch_size: nil,
sub_batch_size: SUB_BATCH_SIZE,
gitlab_schema: nil
@@ -105,6 +106,7 @@ module Gitlab
column_name: batch_column_name,
job_arguments: job_arguments,
interval: job_interval,
+ pause_ms: pause_ms,
min_value: batch_min_value,
max_value: batch_max_value,
batch_class_name: batch_class_name,
diff --git a/lib/gitlab/database/migrations/milestone_mixin.rb b/lib/gitlab/database/migrations/milestone_mixin.rb
index 10bc0c192e7..7d78f74d237 100644
--- a/lib/gitlab/database/migrations/milestone_mixin.rb
+++ b/lib/gitlab/database/migrations/milestone_mixin.rb
@@ -19,11 +19,10 @@ module Gitlab
end
end
- def initialize(name = class_name, version = nil, type = nil)
- raise MilestoneNotSetError, "Milestone is not set for #{self.class.name}" if milestone.nil?
+ def initialize(name = self.class.name, version = nil, _type = nil)
+ raise MilestoneNotSetError, "Milestone is not set for #{name}" if milestone.nil?
super(name, version)
- @version = Gitlab::Database::Migrations::Version.new(version, milestone, type)
end
def milestone # rubocop:disable Lint/DuplicateMethods
diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb
index bb70d052e3e..83cd446534c 100644
--- a/lib/gitlab/database/partitioning/partition_manager.rb
+++ b/lib/gitlab/database/partitioning/partition_manager.rb
@@ -89,6 +89,8 @@ module Gitlab
Gitlab::AppLogger.info(message: "Created partition",
partition_name: partition.partition_name,
table_name: partition.table)
+
+ lock_partitions_for_writes(partition) if should_lock_for_writes?
end
model.partitioning_strategy.after_adding_partitions
@@ -205,6 +207,23 @@ module Gitlab
end
end
end
+
+ def should_lock_for_writes?
+ Feature.enabled?(:automatic_lock_writes_on_partition_tables, type: :ops) &&
+ Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES &&
+ connection != model.connection
+ end
+ strong_memoize_attr :should_lock_for_writes?
+
+ def lock_partitions_for_writes(partition)
+ table_name = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition.partition_name}"
+ Gitlab::Database::LockWritesManager.new(
+ table_name: table_name,
+ connection: connection,
+ database_name: @connection_name,
+ with_retries: !connection.transaction_open?
+ ).lock_writes
+ end
end
end
end
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/database/query_analyzers/ci/partitioning_routing_analyzer.rb b/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
index eb55ebc7619..c2f94b7b0e6 100644
--- a/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
+++ b/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
@@ -8,7 +8,7 @@ module Gitlab
class PartitioningRoutingAnalyzer < Database::QueryAnalyzers::Base
RoutingTableNotUsedError = Class.new(QueryAnalyzerError)
- ENABLED_TABLES = %w[ci_builds_metadata].freeze
+ ENABLED_TABLES = %w[ci_builds ci_builds_metadata].freeze
class << self
def enabled?
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb
new file mode 100644
index 00000000000..583aceba098
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch < Base
+ SetOperatorStarError = Class.new(QueryAnalyzerError)
+
+ DETECT_REGEX = /.*SELECT.+(UNION|EXCEPT|INTERSECT)/i
+
+ class << self
+ def enabled?
+ ::Feature::FlipperFeature.table_exists? &&
+ Feature.enabled?(:query_analyzer_gitlab_schema_metrics, type: :ops)
+ end
+
+ def analyze(parsed)
+ return unless requires_detection?(parsed.sql)
+
+ # Only handle SELECT queries.
+ parsed.pg.tree.stmts.each do |stmt|
+ select_stmt = next_select_stmt(stmt)
+ next unless select_stmt
+
+ types = SelectStmt.new(select_stmt).types
+
+ raise SetOperatorStarError if types.any?(Type::INVALID)
+ end
+ end
+
+ private
+
+ def next_select_stmt(node)
+ return unless node.stmt.respond_to?(:select_stmt)
+
+ node.stmt.select_stmt
+ end
+
+ # This not entirely correct and will run true on `SELECT union_station, ...`
+ def requires_detection?(sql)
+ sql.match DETECT_REGEX
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb
new file mode 100644
index 00000000000..87120b8ffce
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # Columns refer to table columns produced by queries and parts of queries.
+ # If we have `SELECT namespaces.id` then `id` is a column. But, we can also have
+ # `WHERE namespaces.id > 10` and `id` is also a column.
+ #
+ # In static analysis of a SQL query a column source can be ambiguous.
+ # Such as in `SELECT id FROM users, namespaces. In such cases we assume `id` could come from either `users` or
+ # `namespaces`.
+ class Columns
+ class << self
+ # Determine the type of each column in the select statement.
+ # Returns a Set object containing a Types enum.
+ # When an error is found parsing will return immediately.
+ def types(select_stmt)
+ # Forward through any errors when the column refers to a part of the SQL query that is known to include
+ # errors. For example, the column may refer to a column from a CTE that was invalid.
+ return Set.new([Type::INVALID]) if References.errors?(select_stmt.all_references)
+
+ types = Set.new
+
+ # Resolve the type of reference for each target in the select statement.
+ target_list = select_stmt.node.target_list
+ targets = target_list.map(&:res_target)
+ targets.each do |target|
+ target_type = get_target_type(target, select_stmt)
+
+ # A NULL target is of the form:
+ # SELECT NULL::namespaces FROM namespaces
+ types += if Targets.null?(target)
+ # Maintain any errors but otherwise ignore this target.
+ target_type & [Type::INVALID]
+ else
+ target_type
+ end
+ end
+
+ types
+ end
+
+ private
+
+ def get_target_type(target, select_stmt)
+ target_ref_names = Targets.reference_names(target, select_stmt)
+
+ resolved_refs = References.resolved(select_stmt.all_references)
+
+ # Cross reference column references with resolved references.
+ # A resolved reference is part of a SQL query that we were able to analyze already.
+ # A CTE or sub-query would be such a case. The only non-resolvable reference is a table.
+ all_resolved = (target_ref_names - resolved_refs.keys).empty?
+
+ # Is this target `*` such as `SELECT *`.
+ a_star = Targets.a_star?(target)
+
+ if all_resolved
+ # Defer to the reference source types.
+ col_refs = resolved_refs.slice(*target_ref_names)
+ .values
+ .reduce(:union) || Set.new
+
+ if a_star
+ # When * the target forwards through the types of the references.
+ col_refs
+ else
+ # When not * the column is static, but we also forward through any nested errors.
+ (col_refs.to_a & [Type::INVALID]) << Type::STATIC
+ end
+ elsif a_star
+ # This is a * on a table. The * lookup occurs dynamically during query runtime and will
+ # change when the table schema changes.
+ [Type::DYNAMIC]
+ else
+ # This references a column on a table or intermediate result set such as:
+ # SELECT namespaces.id FROM namespaces
+ #
+ # or:
+ # WITH some_cte AS ( ... ) SELECT some_cte.id FROM some_cte
+ [Type::STATIC]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb
new file mode 100644
index 00000000000..0ab58ff7c6f
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# The CTE in a SELECT can reference CTEs defined by the current scope, but also CTEs defined by earlier scopes.
+# With the following query as an example:
+#
+# WITH some_cte AS (select 1)
+# SELECT *
+# FROM (SELECT * FROM some_cte) subquery
+#
+# The CTE some_cte is visible from within the subquery scope.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class CommonTableExpressions
+ class << self
+ # Convert CTEs available within this SELECT statement into a set of References.
+ #
+ # @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
+ # @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
+ def references(node, cte_refs)
+ return cte_refs if node&.with_clause.nil?
+
+ refs = cte_refs.dup
+
+ node.with_clause.ctes.each do |cte|
+ cte_name = name(cte)
+ cte_select_stmt = select_stmt(cte)
+
+ # Resolve the CTE type to dynamic/static/error.
+ refs[cte_name] = if node.with_clause.recursive
+ # Recursive CTEs need special handling to avoid infinite loops.
+ recursive_refs(cte_refs, cte_name, cte_select_stmt)
+ else
+ SelectStmt.new(cte_select_stmt, cte_refs).types
+ end
+ end
+
+ refs
+ end
+
+ private
+
+ def name(cte)
+ cte.common_table_expr.ctename
+ end
+
+ def select_stmt(cte)
+ cte.common_table_expr.ctequery.select_stmt
+ end
+
+ # Return whether the recursive CTE is dynamic/static/error.
+ def recursive_refs(cte_refs, cte_name, select_stmt)
+ # Resolve the non-recursive term before the recursive term.
+ larg_select_stmt = SelectStmt.new(select_stmt.larg, cte_refs)
+ larg_type = larg_select_stmt.types
+ new_cte_refs = cte_refs.merge({ cte_name => larg_type })
+
+ # Now we can resolve the recursive side.
+ rarg_type = SelectStmt.new(select_stmt.rarg, new_cte_refs).types
+
+ final_type = larg_type | rarg_type
+ if final_type.count > 1
+ final_type | [Type::INVALID]
+ else
+ final_type
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb
new file mode 100644
index 00000000000..c205243694a
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class Froms
+ class << self
+ # Parse the FROM part of the SELECT. Construct a mapping of FROM names to their PgQuery node. Recurse any
+ # sub-queries and resolve to a Set of dynamic/static/error.
+ #
+ # Whenever a node is aliased, use the alias name as it's reference and ignore it's original name.
+ #
+ # For example, given:
+ #
+ # SELECT id
+ # FROM namespaces ns
+ #
+ # Return a Hash of { 'ns' => NodeObject }
+ #
+ # @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
+ # @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
+ #
+ # @return [Hash] name of from references mapped to the node that defines their value, or Set if already
+ # resolved.
+ def references(node, cte_refs)
+ refs = {}
+
+ return refs unless node
+
+ node.from_clause.each do |from|
+ range_var = Node.dig(from, :range_var)
+ range_sq = Node.dig(from, :range_subselect)
+
+ if range_var
+ # FROM some_table
+ # FROM some_table some_alias
+ refs.merge!(range_var_reference(range_var, cte_refs))
+ elsif Node.dig(from, :join_expr)
+ # FROM some_table INNER JOIN other_table
+ range_vars = Node.locate_descendants(from, :range_var)
+ range_vars.each do |range_var|
+ refs.merge!(range_var_reference(range_var, cte_refs))
+ end
+ elsif range_sq
+ # FROM (SELECT ...) some_alias
+ select_stmt = Node.dig(range_sq, :subquery, :select_stmt)
+ refs[range_sq.alias.aliasname] = SelectStmt.new(select_stmt, cte_refs).types
+ end
+ end
+
+ refs
+ end
+
+ private
+
+ def range_var_reference(range_var, cte_refs)
+ relname = Node.dig(range_var, :alias, :aliasname) || range_var.relname
+ reference = cte_refs[range_var.relname] || range_var
+
+ { relname => reference }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb
new file mode 100644
index 00000000000..ee41eaa9d3a
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # The Node class allows us to traverse PgQuery nodes with tree like semantics.
+ #
+ # This class balances convenience and performance. The PgQuery nodes are Google::Protobuf::MessageExts which
+ # contain a dynamic set of attributes known as fields. Accessing these fields can cause performance problems
+ # due to the large volume of iterable fields.
+ #
+ # When possible use #dig over the *descendant* methods.
+ #
+ # The filter available to each method reduces the traversed attributes. The default filter only traverses nodes
+ # required to parse for set operator mismatches.
+ class Node
+ class << self
+ include Gitlab::Utils::StrongMemoize
+
+ # The default nodes help speed up traversal. Traversal of other nodes can greatly affect performance.
+ DEFAULT_NODES = %i[
+ a_star
+ alias
+ args
+ column_ref
+ fields
+ func_call
+ join_expr
+ larg
+ range_subselect
+ range_var
+ rarg
+ res_target
+ subquery
+ val
+ ].freeze
+ DEFAULT_FIELD_FILTER = ->(field) { field.is_a?(Integer) || DEFAULT_NODES.include?(field) }.freeze
+
+ # Recurse through children.
+ # The block will yield the child node and the name of that node.
+ # Calling without a block will return an Enumerator.
+ def descendants(node, filter: DEFAULT_FIELD_FILTER, &blk)
+ if blk
+ children(node, filter: filter) do |child_node, child_field|
+ yield(child_node, child_field)
+
+ descendants(child_node, filter: filter, &blk)
+ end
+ nil
+ else
+ enum_for(:descendants, node, filter: filter, &blk)
+ end
+ end
+
+ # Return the first node that matches the field.
+ def locate_descendant(node, field, filter: DEFAULT_FIELD_FILTER)
+ descendants(node, filter: filter).find { |_, child_field| child_field == field }&.first
+ end
+
+ # Return all nodes that match the field.
+ def locate_descendants(node, field, filter: DEFAULT_FIELD_FILTER)
+ descendants(node, filter: filter).select { |_, child_field| child_field == field }.map(&:first)
+ end
+
+ # Like Hash#dig, traverse attributes in sequential order and return the final value.
+ # Return nil if any of the fields are not available.
+ def dig(node, *attrs)
+ obj = node
+ attrs.each do |attr|
+ if obj.respond_to?(attr)
+ obj = obj.public_send(attr) # rubocop:disable GitlabSecurity/PublicSend
+ else
+ obj = nil
+ break
+ end
+ end
+ obj
+ end
+
+ private
+
+ # Interface with a PgQuery result as though it was a tree node.
+ # All elements in a PgQuery result are ancestors of Google::Protobuf::AbstractMessage
+ #
+ # Based off PgQuery's treewalker https://github.com/pganalyze/pg_query/blob/main/lib/pg_query/treewalker.rb
+ def children(node, filter: DEFAULT_FIELD_FILTER, &_blk)
+ attributes = case node
+ when Google::Protobuf::MessageExts
+ descriptor_fields(node.class.descriptor)
+ when Google::Protobuf::RepeatedField
+ node.count.times.to_a
+ end
+
+ attributes.select(&filter).each do |attr|
+ attr_key = attr.is_a?(Symbol) ? attr.to_s : attr
+ child = node[attr_key]
+ next if child.nil?
+
+ yield(child, attr)
+ end
+ end
+
+ def descriptor_fields(descriptor)
+ strong_memoize_with(:descriptor_fields, descriptor) do
+ keys = []
+ descriptor.each do |field|
+ keys << field.name.to_sym
+ end
+ keys
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb
new file mode 100644
index 00000000000..ba6e9752905
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+# References form the base data structure of the PreventSetOperatorMismatch query analyzer.
+#
+# A reference refers to a table, CTE, or other named entity in a SQL query. References are a set of mappings between the
+# name of the reference and the PgQuery node that represents that reference in the parsed tree.
+#
+# Given the SQL:
+#
+# WITH some_cte AS (SELECT 1)
+# SELECT *
+# FROM some_cte, users, namespace ns
+#
+# The reference names would be `some_cte`, `users`, `ns`. The reference values are the nodes in the parse tree that
+# represent that reference:
+# - some_cte: the common table expression node
+# - users: nil, being a table
+# - ns: nil, being a table, but importantly we use the alias name
+#
+# A reference can be "resolved". A resolved reference value is a Set of Types. The reference value was a select
+# statement that has since been parsed.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class References
+ class << self
+ # All references that have already been parsed to determine static/dynamic/error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def resolved(refs)
+ refs.select { |_name, ref| ref.is_a?(Set) }
+ end
+
+ # All references that have not been parsed to determine static/dynamic/error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def unresolved(refs)
+ refs.select { |_name, ref| unresolved?(ref) }
+ end
+
+ # Whether any currently resolved references have resulted in an error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def errors?(refs)
+ resolved(refs).any? { |_, values| values.include?(Type::INVALID) }
+ end
+
+ private
+
+ def resolved?(ref)
+ ref.is_a?(Set)
+ end
+
+ def unresolved?(ref)
+ !resolved?(ref) && table?(ref)
+ end
+
+ def table?(ref)
+ !ref.is_a?(PgQuery::RangeVar)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb
new file mode 100644
index 00000000000..bdbcc49f63f
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class SelectStmt
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :node, :cte_references, :all_references
+
+ # @param [PgQuery::SelectStmt] node The PgQuery node of the select statement.
+ # @param [Hash] inherited_cte_references CTE References available to the select statement.
+ def initialize(node, inherited_cte_references = {})
+ @node = node
+ @cte_references = CommonTableExpressions.references(node, inherited_cte_references)
+ from_references = Froms.references(node, cte_references)
+ @all_references = from_references.merge(cte_references)
+ end
+
+ # returns Set of Types.
+ #
+ # STATIC - queries that don't require a database schema lookup. E.g. `SELECT users.id FROM users`
+ # DYNAMIC - queries that require a database schema lookup. E.g. `SELECT users.* FROM users`
+ # INVALID - set operator queries that mix static and dynamic queries.
+ def types
+ if set_operator?
+ resolve_set_operator_select_types
+ else
+ resolve_normal_select_types
+ end
+ end
+
+ private
+
+ # Standard SELECT, not a set operator (UNION/INTERSECT/EXCEPT)
+ def resolve_normal_select_types
+ # Cross reference resolved sources with what is requested by the SELECT.
+ types = Columns.types(self)
+
+ # Mixed dynamic and static queries can be normalized to simply dynamic queries for the purposes of
+ # detecting mismatched set operator parts.
+ types.delete(Type::STATIC) if types.include?(Type::DYNAMIC)
+
+ types
+ end
+
+ # Set operator (UNION/INTERSECT/EXCEPT)
+ def resolve_set_operator_select_types
+ types = Set.new
+
+ # Recurse each set operator part as a SELECT statement.
+ # select statement part => type
+ set_operator_parts do |part|
+ types += SelectStmt.new(part, cte_references).types
+ end
+
+ types << Type::INVALID if types.count > 1
+
+ types
+ end
+
+ def set_operator?
+ !(node.respond_to?(:op) && node.op == :SETOP_NONE)
+ end
+
+ SET_OPERATOR_PART_LOCATIONS = %i[larg rarg].freeze
+ private_constant :SET_OPERATOR_PART_LOCATIONS
+
+ def set_operator_parts(&_blk)
+ return unless node
+
+ yield node if node.op == :SETOP_NONE
+ yield node.larg if node.larg && node.larg.op == :SETOP_NONE
+ yield node.rarg if node.rarg && node.rarg.op == :SETOP_NONE
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb
new file mode 100644
index 00000000000..99db368efcb
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+# Targets refer to SELECT columns but also JOIN fields, etc.
+# A target can have a qualifying reference to some other entity like a table or CTE.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class Targets
+ class << self
+ # Return the reference names used by the given target.
+ #
+ # For example:
+ # `SELECT users.id` would return ['users']
+ # `SELECT * FROM users, namespaces` would return ['users', 'namespaces']
+ def reference_names(target, select_stmt)
+ # Parse all targets to determine what is referenced.
+ fields = fields(target)
+ case fields.count
+ when 0
+ literal_ref_names(target, select_stmt)
+ when 1
+ unqualified_ref_names(fields, select_stmt)
+ else
+ # The target is qualified such as SELECT reference.id
+ field_ref = fields[fields.count - 2]
+ [field_ref.string.sval]
+ end
+ end
+
+ # True when `SELECT *`
+ def a_star?(target)
+ Node.locate_descendant(target, :a_star)
+ end
+
+ # Null targets are used to produce "polymorphic" query result sets that can be aggregated through a UNION
+ # without having to worry about mismatched columns.
+ #
+ # A null target would be something like:
+ # SELECT NULL::namespaces FROM namespaces
+ def null?(target)
+ target&.val&.type_cast&.arg&.a_const&.isnull
+ end
+
+ private
+
+ def literal_ref_names(target, select_stmt)
+ # The target is unqualified and is not part of a column_ref, such as in `SELECT 1`.
+ # These include targets like literals, functions, and subselects.
+ sub_select_stmt = subselect_select_stmt(target)
+ if sub_select_stmt
+ name = (target.name.presence || "loc_#{target.location}")
+ # The select is anonymous, so we provide a name.
+ k = "#{name}_subselect"
+ # Force parsing of the select.
+ # We don't care about the static/dynamic nature in this case, but we do need to parse for
+ # any nested error states.
+ sub_select = SelectStmt.new(sub_select_stmt, select_stmt.cte_references)
+ select_stmt.all_references[k] = sub_select.types
+ [k]
+ else
+ # TODO we need to parse function references. Assuming no sources for now.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428102
+ []
+ end
+ end
+
+ def unqualified_ref_names(fields, select_stmt)
+ # The target is unqualified, but is part of a column_ref.
+ # E.g. `SELECT id FROM namespaces` or `SELECT namespaces FROM namespaces`
+
+ # Otherwise, check all FROM/JOIN/CTE entries.
+ field = fields[0]
+ field_sval = field&.string&.sval
+ if field_sval && select_stmt.all_references.key?(field_sval)
+ # SELECT some_table_name
+ [field.string.sval]
+ else
+ # SELECT *
+ # SELECT some_column
+ select_stmt.all_references.keys
+ end
+ end
+
+ def fields(target)
+ Node.locate_descendants(target, :fields).flatten
+ end
+
+ def subselect_select_stmt(target)
+ Node.dig(target, :val, :sub_link, :subselect, :select_stmt)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb
new file mode 100644
index 00000000000..5988f963827
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # An enumerated set of constants that represent the state of the parse.
+ module Type
+ STATIC = :static
+ DYNAMIC = :dynamic
+ INVALID = :invalid
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_cache_with_renamed_table.rb b/lib/gitlab/database/schema_cache_with_renamed_table.rb
index 6da76803f7c..e110fc44b7b 100644
--- a/lib/gitlab/database/schema_cache_with_renamed_table.rb
+++ b/lib/gitlab/database/schema_cache_with_renamed_table.rb
@@ -11,26 +11,26 @@ module Gitlab
clear_renamed_tables_cache!
end
- def clear_data_source_cache!(name)
- super(name)
+ def clear_data_source_cache!(connection, table_name)
+ super(connection, table_name)
clear_renamed_tables_cache!
end
- def primary_keys(table_name)
- super(underlying_table(table_name))
+ def primary_keys(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def columns(table_name)
- super(underlying_table(table_name))
+ def columns(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def columns_hash(table_name)
- super(underlying_table(table_name))
+ def columns_hash(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def indexes(table_name)
- super(underlying_table(table_name))
+ def indexes(connection, table_name)
+ super(connection, underlying_table(table_name))
end
private
@@ -40,7 +40,7 @@ module Gitlab
end
def renamed_tables_cache
- @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, new_name|
+ @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, _new_name|
connection.view_exists?(old_name)
end
end
diff --git a/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb b/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb
new file mode 100644
index 00000000000..acc9bbd0aff
--- /dev/null
+++ b/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # This is a legacy extension targeted at Rails versions prior to 7.1
+ # In Rails 7.1, the method parameters have been changed to (connection, table_name)
+ module SchemaCacheWithRenamedTableLegacy
+ # Override methods in ActiveRecord::ConnectionAdapters::SchemaCache
+
+ def clear!
+ super
+
+ clear_renamed_tables_cache!
+ end
+
+ def clear_data_source_cache!(name)
+ super(name)
+
+ clear_renamed_tables_cache!
+ end
+
+ def primary_keys(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def columns(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def columns_hash(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def indexes(table_name)
+ super(underlying_table(table_name))
+ end
+
+ private
+
+ def underlying_table(table_name)
+ renamed_tables_cache.fetch(table_name, table_name)
+ end
+
+ def renamed_tables_cache
+ @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, _new_name|
+ connection.view_exists?(old_name)
+ end
+ end
+
+ def clear_renamed_tables_cache!
+ @renamed_tables = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/tables_locker.rb b/lib/gitlab/database/tables_locker.rb
index aa880b709fe..608dea9e3c5 100644
--- a/lib/gitlab/database/tables_locker.rb
+++ b/lib/gitlab/database/tables_locker.rb
@@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesLocker
- GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_embedding gitlab_geo].freeze
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_embedding gitlab_geo gitlab_jh].freeze
def initialize(logger: nil, dry_run: false, include_partitions: true)
@logger = logger
diff --git a/lib/gitlab/database/tables_truncate.rb b/lib/gitlab/database/tables_truncate.rb
index f91146fff3d..5394dee6fec 100644
--- a/lib/gitlab/database/tables_truncate.rb
+++ b/lib/gitlab/database/tables_truncate.rb
@@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesTruncate
- GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding].freeze
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding gitlab_jh].freeze
def initialize(database_name:, min_batch_size: 5, logger: nil, until_table: nil, dry_run: false)
@database_name = database_name
diff --git a/lib/gitlab/discussions_diff/file_collection.rb b/lib/gitlab/discussions_diff/file_collection.rb
index 60b3a1738f1..3d1f7ab86b3 100644
--- a/lib/gitlab/discussions_diff/file_collection.rb
+++ b/lib/gitlab/discussions_diff/file_collection.rb
@@ -25,8 +25,9 @@ module Gitlab
#
# - Highlight cache is written just for uncached diff files
# - The cache content is not updated (there's no need to do so)
- def load_highlight
- ids = highlightable_collection_ids
+ # - Load only the related diff note ids
+ def load_highlight(diff_note_ids: nil)
+ ids = highlightable_collection_ids(diff_note_ids)
return if ids.empty?
cached_content = read_cache(ids)
@@ -47,8 +48,13 @@ module Gitlab
private
- def highlightable_collection_ids
- each.with_object([]) { |file, memo| memo << file.id unless file.resolved_at }
+ def highlightable_collection_ids(diff_note_ids)
+ each.with_object([]) do |file, memo|
+ # We ignore if file is resolved, or not part of the highlight requested notes
+ next if file.resolved_at || (diff_note_ids.present? && diff_note_ids.exclude?(file.diff_note_id))
+
+ memo << file.id
+ end
end
def read_cache(ids)
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
index ebc4e9c2c8c..e3249b143c8 100644
--- a/lib/gitlab/email/handler/service_desk_handler.rb
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -38,7 +38,7 @@ module Gitlab
create_issue_or_note
if from_address
- add_email_participant
+ add_email_participants
send_thank_you_email unless reply_email?
end
end
@@ -215,6 +215,10 @@ module Gitlab
end
strong_memoize_attr :to_address
+ def cc_addresses
+ mail.cc || []
+ end
+
def can_handle_legacy_format?
project_path && project_path.include?('/') && !mail_key.include?('+')
end
@@ -223,11 +227,33 @@ module Gitlab
Users::Internal.support_bot
end
- def add_email_participant
+ def add_email_participants
return if reply_email? && !Feature.enabled?(:issue_email_participants, @issue.project)
@issue.issue_email_participants.create(email: from_address)
+
+ add_external_participants_from_cc
+ end
+
+ def add_external_participants_from_cc
+ return if project.service_desk_setting.nil?
+ return unless project.service_desk_setting.add_external_participants_from_cc?
+
+ cc_addresses.each do |email|
+ next if service_desk_addresses.include?(email)
+
+ @issue.issue_email_participants.create!(email: email)
+ end
+ end
+
+ def service_desk_addresses
+ [
+ project.service_desk_incoming_address,
+ project.service_desk_alias_address,
+ project.service_desk_custom_address
+ ].compact
end
+ strong_memoize_attr :service_desk_addresses
end
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 7d47bfe88fe..1a7a2fba2f3 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -6,7 +6,7 @@ module Gitlab
# When updating emoji assets increase the version below
# and update the version number in `app/assets/javascripts/emoji/index.js`
- EMOJI_VERSION = 2
+ EMOJI_VERSION = 3
# Return a Pathname to emoji's current versioned folder
#
diff --git a/lib/gitlab/encrypted_command_base.rb b/lib/gitlab/encrypted_command_base.rb
index b35c28b85cd..679d9d8e31a 100644
--- a/lib/gitlab/encrypted_command_base.rb
+++ b/lib/gitlab/encrypted_command_base.rb
@@ -7,12 +7,12 @@ module Gitlab
EDIT_COMMAND_NAME = "base"
class << self
- def encrypted_secrets
+ def encrypted_secrets(**args)
raise NotImplementedError
end
- def write(contents)
- encrypted = encrypted_secrets
+ def write(contents, args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
validate_contents(contents)
@@ -25,8 +25,8 @@ module Gitlab
warn "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?"
end
- def edit
- encrypted = encrypted_secrets
+ def edit(args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
if ENV["EDITOR"].blank?
@@ -58,8 +58,8 @@ module Gitlab
temp_file&.unlink
end
- def show
- encrypted = encrypted_secrets
+ def show(args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
puts encrypted.read.presence || "File '#{encrypted.content_path}' does not exist. Use `gitlab-rake #{self::EDIT_COMMAND_NAME}` to change that."
diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb
index 6b64281e631..5ead57e17fd 100644
--- a/lib/gitlab/encrypted_configuration.rb
+++ b/lib/gitlab/encrypted_configuration.rb
@@ -30,7 +30,7 @@ module Gitlab
end
def initialize(content_path: nil, base_key: nil, previous_keys: [])
- @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } if content_path
+ @content_path = Pathname.new(content_path).then { |path| path.symlink? ? path.realpath : path } if content_path
@key = self.class.generate_key(base_key) if base_key
@previous_keys = previous_keys
end
diff --git a/lib/gitlab/encrypted_ldap_command.rb b/lib/gitlab/encrypted_ldap_command.rb
index 5e1eabe7ec6..442c675f19e 100644
--- a/lib/gitlab/encrypted_ldap_command.rb
+++ b/lib/gitlab/encrypted_ldap_command.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-# rubocop:disable Rails/Output
module Gitlab
class EncryptedLdapCommand < EncryptedCommandBase
DISPLAY_NAME = "LDAP"
@@ -21,4 +20,3 @@ module Gitlab
end
end
end
-# rubocop:enable Rails/Output
diff --git a/lib/gitlab/encrypted_redis_command.rb b/lib/gitlab/encrypted_redis_command.rb
new file mode 100644
index 00000000000..608edcdb950
--- /dev/null
+++ b/lib/gitlab/encrypted_redis_command.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# rubocop:disable Rails/Output
+module Gitlab
+ class EncryptedRedisCommand < EncryptedCommandBase
+ DISPLAY_NAME = "Redis"
+ EDIT_COMMAND_NAME = "gitlab:redis:secret:edit"
+
+ class << self
+ def all_redis_instance_class_names
+ Gitlab::Redis::ALL_CLASSES.map do |c|
+ normalized_instance_name(c)
+ end
+ end
+
+ def normalized_instance_name(instance)
+ if instance.is_a?(Class)
+ # Gitlab::Redis::SharedState => sharedstate
+ instance.name.demodulize.to_s.downcase
+ else
+ # Drop all hyphens, underscores, and spaces from the name
+ # eg.: shared_state => sharedstate
+ instance.gsub(/[-_ ]/, '').downcase
+ end
+ end
+
+ def encrypted_secrets(**args)
+ if args[:instance_name]
+ instance_class = Gitlab::Redis::ALL_CLASSES.find do |instance|
+ normalized_instance_name(instance) == normalized_instance_name(args[:instance_name])
+ end
+
+ unless instance_class
+ error_message = <<~MSG
+ Specified instance name #{args[:instance_name]} does not exist.
+ The available instances are #{all_redis_instance_class_names.join(', ')}."
+ MSG
+
+ raise error_message
+ end
+ else
+ instance_class = Gitlab::Redis::Cache
+ end
+
+ instance_class.encrypted_secrets
+ end
+
+ def encrypted_file_template
+ <<~YAML
+ # password: '123'
+ YAML
+ end
+ end
+ end
+end
+# rubocop:enable Rails/Output
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 13959f6aa68..ef8f2d4d61b 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -21,7 +21,7 @@ module Gitlab
# Configuration files
gitignore: '.gitignore',
- gitlab_ci: '.gitlab-ci.yml',
+ gitlab_ci: ::Ci::Pipeline::DEFAULT_CONFIG_PATH,
route_map: '.gitlab/route-map.yml',
# Dependency files
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 3d2bde6f0a7..e134fb31879 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -4,7 +4,6 @@ module Gitlab
module Git
class Blame
include Gitlab::EncodingHelper
- include Gitlab::Git::WrapsGitalyErrors
attr_reader :lines, :blames, :range
@@ -35,11 +34,9 @@ module Gitlab
end
def fetch_raw_blame
- wrapped_gitaly_errors do
- @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec)
- end
- # Return empty result when blame range is out-of-range
+ @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec)
rescue ArgumentError
+ # Return an empty result when the blame range is out-of-range or path is not found
""
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index ae90291c0a3..3744c81f51d 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -230,5 +230,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Blob.singleton_class.prepend Gitlab::Git::RuggedImpl::Blob::ClassMethods
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 571dde6fcfc..1086ea45a7a 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -5,7 +5,6 @@ module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
- prepend Gitlab::Git::RuggedImpl::Commit
extend Gitlab::Git::WrapsGitalyErrors
include Gitlab::Utils::StrongMemoize
@@ -502,5 +501,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Commit.singleton_class.prepend Gitlab::Git::RuggedImpl::Commit::ClassMethods
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index 4a09f866db4..205dd5be35a 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -4,7 +4,6 @@ module Gitlab
module Git
class Ref
include Gitlab::EncodingHelper
- include Gitlab::Git::RuggedImpl::Ref
# Branch or tag name
# without "refs/tags|heads" prefix
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index a98cf95edf4..db6e6b4d00b 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -11,7 +11,6 @@ module Gitlab
include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
- prepend Gitlab::Git::RuggedImpl::Repository
SEARCH_CONTEXT_LINES = 3
REV_LIST_COMMIT_LIMIT = 2_000
diff --git a/lib/gitlab/git/rugged_impl/blob.rb b/lib/gitlab/git/rugged_impl/blob.rb
deleted file mode 100644
index dc869ff5279..00000000000
--- a/lib/gitlab/git/rugged_impl/blob.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Blob
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- override :tree_entry
- def tree_entry(repository, sha, path, limit)
- if use_rugged?(repository, :rugged_tree_entry)
- execute_rugged_call(:rugged_tree_entry, repository, sha, path, limit)
- else
- super
- end
- end
-
- private
-
- def rugged_tree_entry(repository, sha, path, limit)
- return unless path
-
- # Strip any leading / characters from the path
- path = path.sub(%r{\A/*}, '')
-
- rugged_commit = repository.lookup(sha)
- root_tree = rugged_commit.tree
-
- blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
-
- return unless blob_entry
-
- if blob_entry[:type] == :commit
- submodule_blob(blob_entry, path, sha)
- else
- blob = repository.lookup(blob_entry[:oid])
-
- if blob
- new(
- id: blob.oid,
- name: blob_entry[:name],
- size: blob.size,
- # Rugged::Blob#content is expensive; don't call it if we don't have to.
- data: limit == 0 ? '' : blob.content(limit),
- mode: blob_entry[:filemode].to_s(8),
- path: path,
- commit_id: sha,
- binary: blob.binary?
- )
- end
- end
- rescue Rugged::ReferenceError
- nil
- end
-
- # Recursive search of blob id by path
- #
- # Ex.
- # blog/ # oid: 1a
- # app/ # oid: 2a
- # models/ # oid: 3a
- # file.rb # oid: 4a
- #
- #
- # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
- #
- def find_entry_by_path(repository, root_id, *path_parts)
- root_tree = repository.lookup(root_id)
-
- entry = root_tree.find do |entry|
- entry[:name] == path_parts[0]
- end
-
- return unless entry
-
- if path_parts.size > 1
- return unless entry[:type] == :tree
-
- path_parts.shift
- find_entry_by_path(repository, entry[:oid], *path_parts)
- else
- [:blob, :commit].include?(entry[:type]) ? entry : nil
- end
- end
-
- def submodule_blob(blob_entry, path, sha)
- new(
- id: blob_entry[:oid],
- name: blob_entry[:name],
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/commit.rb b/lib/gitlab/git/rugged_impl/commit.rb
deleted file mode 100644
index cf547414b0d..00000000000
--- a/lib/gitlab/git/rugged_impl/commit.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-# rubocop:disable Gitlab/ModuleWithInstanceVariables
-module Gitlab
- module Git
- module RuggedImpl
- module Commit
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- def rugged_find(repo, commit_id)
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(::Rugged::Commit) ? obj : nil
- rescue ::Rugged::Error
- nil
- end
-
- # This needs to return an array of Gitlab::Git:Commit objects
- # instead of Rugged::Commit objects to ensure upstream models
- # operate on a consistent interface. Unlike
- # Gitlab::Git::Commit.find, Gitlab::Git::Commit.batch_by_oid
- # doesn't attempt to decorate the result.
- def rugged_batch_by_oid(repo, oids)
- oids.map { |oid| rugged_find(repo, oid) }
- .compact
- .map { |commit| decorate(repo, commit) }
- # Match Gitaly's list_commits_by_oid behavior
- rescue ::Gitlab::Git::Repository::NoRepository
- []
- end
-
- override :find_commit
- def find_commit(repo, commit_id)
- if use_rugged?(repo, :rugged_find_commit)
- execute_rugged_call(:rugged_find, repo, commit_id)
- else
- super
- end
- end
-
- override :batch_by_oid
- def batch_by_oid(repo, oids)
- if use_rugged?(repo, :rugged_list_commits_by_oid)
- execute_rugged_call(:rugged_batch_by_oid, repo, oids)
- else
- super
- end
- end
- end
-
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- override :init_commit
- def init_commit(raw_commit)
- case raw_commit
- when ::Rugged::Commit
- init_from_rugged(raw_commit)
- else
- super
- end
- end
-
- override :commit_tree_entry
- def commit_tree_entry(path)
- if use_rugged?(@repository, :rugged_commit_tree_entry)
- execute_rugged_call(:rugged_tree_entry, path)
- else
- super
- end
- end
-
- # Is this the same as Blob.find_entry_by_path ?
- def rugged_tree_entry(path)
- rugged_commit.tree.path(path)
- rescue Rugged::TreeError
- nil
- end
-
- def rugged_commit
- @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
- raw_commit
- else
- @repository.rev_parse_target(id)
- end
- end
-
- def init_from_rugged(commit)
- author = commit.author
- committer = commit.committer
-
- @raw_commit = commit
- @id = commit.oid
- @message = commit.message
- @authored_date = author[:time]
- @committed_date = committer[:time]
- @author_name = author[:name]
- @author_email = author[:email]
- @committer_name = committer[:name]
- @committer_email = committer[:email]
- @parent_ids = commit.parents.map(&:oid)
- @trailers = Hash[commit.trailers]
- end
- end
- end
- end
-end
-# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/gitlab/git/rugged_impl/ref.rb b/lib/gitlab/git/rugged_impl/ref.rb
deleted file mode 100644
index b553e82dc47..00000000000
--- a/lib/gitlab/git/rugged_impl/ref.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Ref
- def self.dereference_object(object)
- object = object.target while object.is_a?(::Rugged::Tag::Annotation)
-
- object
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/repository.rb b/lib/gitlab/git/rugged_impl/repository.rb
deleted file mode 100644
index cd4eefa158e..00000000000
--- a/lib/gitlab/git/rugged_impl/repository.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-# rubocop:disable Gitlab/ModuleWithInstanceVariables
-module Gitlab
- module Git
- module RuggedImpl
- module Repository
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- FEATURE_FLAGS = %i[rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry rugged_list_commits_by_oid].freeze
-
- def alternate_object_directories
- relative_object_directories.map { |d| File.join(path, d) }
- end
-
- ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES = %w[
- GIT_OBJECT_DIRECTORY_RELATIVE
- GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
- ].freeze
-
- def relative_object_directories
- Gitlab::Git::HookEnv.all(gl_repository).values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
- end
-
- def rugged
- @rugged ||= ::Rugged::Repository.new(path, alternates: alternate_object_directories)
- rescue ::Rugged::RepositoryError, ::Rugged::OSError
- raise ::Gitlab::Git::Repository::NoRepository, 'no repository for such path'
- end
-
- def cleanup
- @rugged&.close
- end
-
- # Return the object that +revspec+ points to. If +revspec+ is an
- # annotated tag, then return the tag's target instead.
- def rev_parse_target(revspec)
- obj = rugged.rev_parse(revspec)
- Ref.dereference_object(obj)
- end
-
- override :ancestor?
- def ancestor?(from, to)
- if use_rugged?(self, :rugged_commit_is_ancestor)
- execute_rugged_call(:rugged_is_ancestor?, from, to)
- else
- super
- end
- end
-
- def rugged_is_ancestor?(ancestor_id, descendant_id)
- return false if ancestor_id.nil? || descendant_id.nil?
-
- rugged_merge_base(ancestor_id, descendant_id) == ancestor_id
- rescue Rugged::OdbError
- false
- end
-
- def rugged_merge_base(from, to)
- rugged.merge_base(from, to)
- rescue Rugged::ReferenceError
- nil
- end
-
- # Lookup for rugged object by oid or ref name
- def lookup(oid_or_ref_name)
- rev_parse_target(oid_or_ref_name)
- end
- end
- end
- end
-end
-# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/gitlab/git/rugged_impl/tree.rb b/lib/gitlab/git/rugged_impl/tree.rb
deleted file mode 100644
index bc3ff01e1e2..00000000000
--- a/lib/gitlab/git/rugged_impl/tree.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Tree
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
-
- override :tree_entries
- def tree_entries(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, pagination_params = nil)
- if use_rugged?(repository, :rugged_tree_entries)
- entries = execute_rugged_call(
- :tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
-
- if pagination_params
- paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
- else
- [entries, nil]
- end
- else
- super
- end
- end
-
- # Rugged version of TreePagination in Go: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3611
- def paginated_response(entries, limit, token)
- total_entries = entries.count
-
- return [[], nil] if limit == 0 || limit.blank?
-
- entries = Gitlab::Utils.stable_sort_by(entries) { |x| TREE_SORT_ORDER[x.type] }
-
- if token.blank?
- index = 0
- else
- index = entries.index { |entry| entry.id == token }
-
- raise Gitlab::Git::CommandError, "could not find starting OID: #{token}" if index.nil?
-
- index += 1
- end
-
- return [entries[index..], nil] if limit < 0
-
- last_index = index + limit
- result = entries[index...last_index]
-
- if last_index < total_entries
- cursor = Gitaly::PaginationCursor.new(next_cursor: result.last.id)
- end
-
- [result, cursor]
- end
-
- def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
- tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
- # This was an optimization to reduce N+1 queries for Gitaly
- # (https://gitlab.com/gitlab-org/gitaly/issues/530).
- rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
- end
- end
-
- def tree_entries_from_rugged(repository, sha, path, recursive)
- current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
- ordered_entries = []
-
- current_path_entries.each do |entry|
- ordered_entries << entry
-
- if recursive && entry.dir?
- ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
- end
- end
-
- ordered_entries
- end
-
- def rugged_populate_flat_path(repository, sha, path, entries)
- entries.each do |entry|
- entry.flat_path = entry.path
-
- next unless entry.dir?
-
- entry.flat_path =
- if path
- File.join(path, rugged_flatten_tree(repository, sha, entry, path))
- else
- rugged_flatten_tree(repository, sha, entry, path)
- end
- end
- end
-
- # Returns the relative path of the first subdir that doesn't have only one directory descendant
- def rugged_flatten_tree(repository, sha, tree, root_path)
- subtree = tree_entries_from_rugged(repository, sha, tree.path, false)
-
- if subtree.count == 1 && subtree.first.dir?
- File.join(tree.name, rugged_flatten_tree(repository, sha, subtree.first, root_path))
- else
- tree.name
- end
- end
-
- def get_tree_entries_from_rugged(repository, sha, path)
- commit = repository.lookup(sha)
- root_tree = commit.tree
-
- tree = if path
- id = find_id_by_path(repository, root_tree.oid, path)
- if id
- repository.lookup(id)
- else
- []
- end
- else
- root_tree
- end
-
- tree.map do |entry|
- current_path = path ? File.join(path, entry[:name]) : entry[:name]
-
- new(
- id: entry[:oid],
- name: entry[:name],
- type: entry[:type],
- mode: entry[:filemode].to_s(8),
- path: current_path,
- commit_id: sha
- )
- end
- rescue Rugged::ReferenceError
- []
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/use_rugged.rb b/lib/gitlab/git/rugged_impl/use_rugged.rb
deleted file mode 100644
index 57cced97d02..00000000000
--- a/lib/gitlab/git/rugged_impl/use_rugged.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Git
- module RuggedImpl
- module UseRugged
- def use_rugged?(_, _)
- false
- end
-
- def execute_rugged_call(method_name, *args)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- start = Gitlab::Metrics::System.monotonic_time
-
- result = send(method_name, *args) # rubocop:disable GitlabSecurity/PublicSend
-
- duration = Gitlab::Metrics::System.monotonic_time - start
-
- if Gitlab::RuggedInstrumentation.active?
- Gitlab::RuggedInstrumentation.increment_query_count
- Gitlab::RuggedInstrumentation.add_query_time(duration)
-
- Gitlab::RuggedInstrumentation.add_call_details(
- feature: method_name,
- args: args,
- duration: duration,
- backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller))
- end
-
- result
- end
- end
-
- def running_puma_with_multiple_threads?
- return false unless Gitlab::Runtime.puma?
-
- ::Puma.respond_to?(:cli_config) && ::Puma.cli_config.options[:max_threads] > 1
- end
-
- def rugged_feature_keys
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS
- end
-
- def rugged_enabled_through_feature_flag?
- false
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index 6e97e412b91..4747ab55c63 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -12,9 +12,6 @@ module Gitlab
class << self
# Get list of tree objects
# for repository based on commit sha and path
- # Uses rugged for raw objects
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
def where(
repository, sha, path = nil, recursive = false, skip_flat_paths = true, rescue_not_found = true,
pagination_params = nil)
@@ -110,5 +107,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Tree.singleton_class.prepend Gitlab::Git::RuggedImpl::Tree::ClassMethods
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 45283d51b1b..72016aa1183 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -101,7 +101,7 @@ module Gitlab
end
def guest_can_download?
- Guest.can?(download_ability, container)
+ ::Users::Anonymous.can?(download_ability, container)
end
def deploy_key_can_download_code?
@@ -395,7 +395,7 @@ module Gitlab
user.can?(:read_project, project)
elsif ci?
false
- end || Guest.can?(:read_project, project)
+ end || ::Users::Anonymous.can?(:read_project, project)
end
def http?
diff --git a/lib/gitlab/git_access_project.rb b/lib/gitlab/git_access_project.rb
index 732e0e14257..b007a957348 100644
--- a/lib/gitlab/git_access_project.rb
+++ b/lib/gitlab/git_access_project.rb
@@ -47,7 +47,7 @@ module Gitlab
end
def repository_path_match
- strong_memoize(:repository_path_match) { repository_path.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
+ strong_memoize(:repository_path_match) { repository_path&.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
end
def ensure_project_on_push!
diff --git a/lib/gitlab/git_audit_event.rb b/lib/gitlab/git_audit_event.rb
deleted file mode 100644
index b8365bdf41f..00000000000
--- a/lib/gitlab/git_audit_event.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- class GitAuditEvent # rubocop:disable Gitlab/NamespacedClass
- attr_reader :project, :user, :author
-
- def initialize(player, project)
- @project = project
- @author = player.is_a?(::API::Support::GitAccessActor) ? player.deploy_key_or_user : player
- @user = player.is_a?(::API::Support::GitAccessActor) ? player.user : player
- end
-
- def send_audit_event(msg)
- return if user.blank? || project.blank?
-
- audit_context = {
- name: 'repository_git_operation',
- stream_only: true,
- author: author,
- scope: project,
- target: project,
- message: msg
- }
-
- ::Gitlab::Audit::Auditor.audit(audit_context)
- end
- end
-end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 5ec58fc4f44..da38c11ebca 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -328,6 +328,8 @@ module Gitlab
'client_name' => CLIENT_NAME
}
+ relative_path = fetch_relative_path
+
context_data = Gitlab::ApplicationContext.current
feature_stack = Thread.current[:gitaly_feature_stack]
@@ -339,6 +341,7 @@ module Gitlab
metadata['username'] = context_data['meta.user'] if context_data&.fetch('meta.user', nil)
metadata['user_id'] = context_data['meta.user_id'].to_s if context_data&.fetch('meta.user_id', nil)
metadata['remote_ip'] = context_data['meta.remote_ip'] if context_data&.fetch('meta.remote_ip', nil)
+ metadata['relative-path-bin'] = relative_path if relative_path
metadata.merge!(Feature::Gitaly.server_feature_flags(**feature_flag_actors))
metadata.merge!(route_to_primary)
@@ -348,6 +351,17 @@ module Gitlab
{ metadata: metadata, deadline: deadline_info[:deadline] }
end
+ # The GitLab `internal/allowed/` API sets the :gitlab_git_relative_path
+ # variable. This provides the repository relative path which can be used to
+ # locate snapshot repositories in Gitaly which act as a quarantine repository
+ # until a transaction is committed.
+ def self.fetch_relative_path
+ return unless Gitlab::SafeRequestStore.active?
+ return if Gitlab::SafeRequestStore[:gitlab_git_relative_path].blank?
+
+ Gitlab::SafeRequestStore.fetch(:gitlab_git_relative_path)
+ end
+
# Gitlab::Git::HookEnv will set the :gitlab_git_env variable in case we're
# running in the context of a Gitaly hook call, which may make use of
# quarantined object directories. We thus need to pass along the path of
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 1ef5b0f96c2..3949e8e6416 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -418,6 +418,15 @@ module Gitlab
response = gitaly_client_call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
response.reduce([]) { |memo, msg| memo << msg.data }.join
+ rescue GRPC::BadStatus => e
+ detailed_error = GitalyClient.decode_detailed_error(e)
+
+ case detailed_error.try(:error)
+ when :out_of_range, :path_not_found
+ raise ArgumentError, e.details
+ else
+ raise e
+ end
end
def find_commit(revision)
diff --git a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
index b1278e3bfac..a6912547ce9 100644
--- a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
+++ b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
@@ -43,11 +43,15 @@ module Gitlab
def conflict_from_gitaly_file_header(header)
{
- ancestor: { path: header.ancestor_path },
- ours: { path: header.our_path, mode: header.our_mode },
- theirs: { path: header.their_path }
+ ancestor: { path: encode_path(header.ancestor_path) },
+ ours: { path: encode_path(header.our_path), mode: header.our_mode },
+ theirs: { path: encode_path(header.their_path) }
}
end
+
+ def encode_path(path)
+ Gitlab::EncodingHelper.encode_utf8(path)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index d92bf5263f1..457380615f7 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -136,10 +136,13 @@ module Gitlab
response.base.presence
end
- def fork_repository(source_repository)
+ def fork_repository(source_repository, branch = nil)
+ revision = branch.present? ? "refs/heads/#{branch}" : ""
+
request = Gitaly::CreateForkRequest.new(
repository: @gitaly_repo,
- source_repository: source_repository.gitaly_repository
+ source_repository: source_repository.gitaly_repository,
+ revision: revision
)
gitaly_client_call(
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 4cc0269673f..adf0c811274 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -31,19 +31,11 @@ module Gitlab
end
def self.disk_access_denied?
- return false if rugged_enabled?
-
!temporarily_allowed?(ALLOW_KEY)
rescue StandardError
false # Err on the side of caution, don't break gitlab for people
end
- def self.rugged_enabled?
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.any? do |flag|
- Feature.enabled?(flag)
- end
- end
-
def initialize(storage)
raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')
diff --git a/lib/gitlab/github_import/attachments_downloader.rb b/lib/gitlab/github_import/attachments_downloader.rb
index 4db55a6aabb..df9c6c8342d 100644
--- a/lib/gitlab/github_import/attachments_downloader.rb
+++ b/lib/gitlab/github_import/attachments_downloader.rb
@@ -29,8 +29,8 @@ module Gitlab
validate_content_length
validate_filepath
- redirection_url = get_assets_download_redirection_url
- file = download_from(redirection_url)
+ download_url = get_assets_download_redirection_url
+ file = download_from(download_url)
validate_symlink
file
@@ -60,16 +60,16 @@ module Gitlab
options[:follow_redirects] = false
response = Gitlab::HTTP.perform_request(Net::HTTP::Get, file_url, options)
- raise_error("expected a redirect response, got #{response.code}") unless response.redirection?
- redirection_url = response.headers[:location]
- filename = URI.parse(redirection_url).path
+ download_url = if response.redirection?
+ response.headers[:location]
+ else
+ file_url
+ end
- unless Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| filename.ends_with?(type) }
- raise UnsupportedAttachmentError
- end
+ file_type_valid?(URI.parse(download_url).path)
- redirection_url
+ download_url
end
def github_assets_url_regex
@@ -89,6 +89,12 @@ module Gitlab
File.join(dir, filename)
end
end
+
+ def file_type_valid?(file_url)
+ return if Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| file_url.ends_with?(type) }
+
+ raise UnsupportedAttachmentError
+ end
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 5a0ae680ab8..33e74c90115 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -182,12 +182,12 @@ module Gitlab
request_count_counter.increment
- raise_or_wait_for_rate_limit unless requests_remaining?
+ raise_or_wait_for_rate_limit('Internal threshold reached') unless requests_remaining?
begin
with_retry { yield }
- rescue ::Octokit::TooManyRequests
- raise_or_wait_for_rate_limit
+ rescue ::Octokit::TooManyRequests => e
+ raise_or_wait_for_rate_limit(e.response_body)
# This retry will only happen when running in sequential mode as we'll
# raise an error in parallel mode.
@@ -213,11 +213,11 @@ module Gitlab
octokit.rate_limit.limit
end
- def raise_or_wait_for_rate_limit
+ def raise_or_wait_for_rate_limit(message)
rate_limit_counter.increment
if parallel?
- raise RateLimitError
+ raise RateLimitError, message
else
sleep(rate_limit_resets_in)
end
diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb
index b960df581e4..0780ba6119f 100644
--- a/lib/gitlab/github_import/issuable_finder.rb
+++ b/lib/gitlab/github_import/issuable_finder.rb
@@ -11,6 +11,7 @@ module Gitlab
# The base cache key to use for storing/retrieving issuable IDs.
CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`.
# object - The object to look up or set a database ID for.
@@ -23,9 +24,18 @@ module Gitlab
#
# This method will return `nil` if no ID could be found.
def database_id
- val = Gitlab::Cache::Import::Caching.read(cache_key, timeout: timeout)
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key, timeout: timeout)
- val.to_i if val.present?
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = cache_key_type.safe_constantize&.find_by(project_id: project.id, iid: cache_key_iid)&.id ||
+ CACHE_OBJECT_NOT_FOUND
+
+ cache_database_id(object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# Associates the given database ID with the current object.
diff --git a/lib/gitlab/github_import/job_delay_calculator.rb b/lib/gitlab/github_import/job_delay_calculator.rb
index 52b211c92d6..077a27df16c 100644
--- a/lib/gitlab/github_import/job_delay_calculator.rb
+++ b/lib/gitlab/github_import/job_delay_calculator.rb
@@ -15,7 +15,7 @@ module Gitlab
def calculate_job_delay(job_index)
multiplier = (job_index / parallel_import_batch[:size])
- (multiplier * parallel_import_batch[:delay]) + 1.second
+ (multiplier * parallel_import_batch[:delay]).to_i + 1
end
end
end
diff --git a/lib/gitlab/github_import/label_finder.rb b/lib/gitlab/github_import/label_finder.rb
index 39e669dbba4..d0bbd2bc7cf 100644
--- a/lib/gitlab/github_import/label_finder.rb
+++ b/lib/gitlab/github_import/label_finder.rb
@@ -7,6 +7,7 @@ module Gitlab
# The base cache key to use for storing/retrieving label IDs.
CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`.
def initialize(project)
@@ -15,7 +16,18 @@ module Gitlab
# Returns the label ID for the given name.
def id_for(name)
- Gitlab::Cache::Import::Caching.read_integer(cache_key_for(name))
+ cache_key = cache_key_for(name)
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key)
+
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = project.labels.with_title(name).pick(:id) || CACHE_OBJECT_NOT_FOUND
+
+ Gitlab::Cache::Import::Caching.write(cache_key, object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -32,7 +44,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def cache_key_for(name)
- CACHE_KEY % { project: project.id, name: name }
+ format(CACHE_KEY, project: project.id, name: name)
end
end
end
diff --git a/lib/gitlab/github_import/milestone_finder.rb b/lib/gitlab/github_import/milestone_finder.rb
index d9290e36ea1..dcb679fda6d 100644
--- a/lib/gitlab/github_import/milestone_finder.rb
+++ b/lib/gitlab/github_import/milestone_finder.rb
@@ -7,6 +7,7 @@ module Gitlab
# The base cache key to use for storing/retrieving milestone IDs.
CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`
def initialize(project)
@@ -18,7 +19,20 @@ module Gitlab
def id_for(issuable)
return unless issuable.milestone_number
- Gitlab::Cache::Import::Caching.read_integer(cache_key_for(issuable.milestone_number))
+ milestone_iid = issuable.milestone_number
+ cache_key = cache_key_for(milestone_iid)
+
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key)
+
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = project.milestones.by_iid(milestone_iid).pick(:id) || CACHE_OBJECT_NOT_FOUND
+
+ Gitlab::Cache::Import::Caching.write(cache_key, object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -35,7 +49,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def cache_key_for(iid)
- CACHE_KEY % { project: project.id, iid: iid }
+ format(CACHE_KEY, project: project.id, iid: iid)
end
end
end
diff --git a/lib/gitlab/github_import/object_counter.rb b/lib/gitlab/github_import/object_counter.rb
index 88e91800cee..5618cfc6044 100644
--- a/lib/gitlab/github_import/object_counter.rb
+++ b/lib/gitlab/github_import/object_counter.rb
@@ -52,7 +52,7 @@ module Gitlab
.sort
.each do |counter|
object_type = counter.split('/').last
- result[operation][object_type] = CACHING.read_integer(counter) || 0
+ result[operation][object_type] = CACHING.read_integer(counter, timeout: IMPORT_CACHING_TIMEOUT) || 0
end
end
end
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index cccd99f48b1..ce93b5203df 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -6,7 +6,7 @@ module Gitlab
include JobDelayCalculator
attr_reader :project, :client, :page_counter, :already_imported_cache_key,
- :job_waiter_cache_key, :job_waiter_remaining_cache_key
+ :job_waiter_cache_key, :job_waiter_remaining_cache_key
# The base cache key to use for tracking already imported objects.
ALREADY_IMPORTED_CACHE_KEY =
@@ -26,12 +26,11 @@ module Gitlab
@client = client
@parallel = parallel
@page_counter = PageCounter.new(project, collection_method)
- @already_imported_cache_key = ALREADY_IMPORTED_CACHE_KEY %
- { project: project.id, collection: collection_method }
- @job_waiter_cache_key = JOB_WAITER_CACHE_KEY %
- { project: project.id, collection: collection_method }
- @job_waiter_remaining_cache_key = JOB_WAITER_REMAINING_CACHE_KEY %
- { project: project.id, collection: collection_method }
+ @already_imported_cache_key = format(ALREADY_IMPORTED_CACHE_KEY, project: project.id,
+ collection: collection_method)
+ @job_waiter_cache_key = format(JOB_WAITER_CACHE_KEY, project: project.id, collection: collection_method)
+ @job_waiter_remaining_cache_key = format(JOB_WAITER_REMAINING_CACHE_KEY, project: project.id,
+ collection: collection_method)
end
def parallel?
@@ -57,7 +56,8 @@ module Gitlab
# still scheduling duplicates while. Since all work has already been
# completed those jobs will just cycle through any remaining pages while
# not scheduling anything.
- Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
+ Gitlab::Cache::Import::Caching.expire(already_imported_cache_key,
+ Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
info(project.id, message: "importer finished")
retval
@@ -97,7 +97,7 @@ module Gitlab
repr = object_representation(object)
job_delay = calculate_job_delay(enqueued_job_counter)
- sidekiq_worker_class.perform_in(job_delay, project.id, repr.to_hash, job_waiter.key)
+ sidekiq_worker_class.perform_in(job_delay, project.id, repr.to_hash.deep_stringify_keys, job_waiter.key.to_s)
enqueued_job_counter += 1
job_waiter.jobs_remaining = Gitlab::Cache::Import::Caching.increment(job_waiter_remaining_cache_key)
diff --git a/lib/gitlab/github_import/representation/to_hash.rb b/lib/gitlab/github_import/representation/to_hash.rb
index 4a0f36ab8f0..54faa51a787 100644
--- a/lib/gitlab/github_import/representation/to_hash.rb
+++ b/lib/gitlab/github_import/representation/to_hash.rb
@@ -16,11 +16,15 @@ module Gitlab
hash
end
+ # This method allow objects to be safely passed directly to Sidekiq without errors.
+ # It returns JSON datatypes: string, integer, float, boolean, null(nil), array and hash.
def convert_value_for_to_hash(value)
if value.is_a?(Array)
value.map { |v| convert_value_for_to_hash(v) }
elsif value.respond_to?(:to_hash)
value.to_hash
+ elsif value.respond_to?(:strftime) || value.is_a?(Symbol)
+ value.to_s
else
value
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index e057b4bb6f1..59813e4f5a0 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -50,6 +50,7 @@ module Gitlab
gon.suggested_label_colors = LabelsHelper.suggested_colors
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
gon.time_display_relative = true
+ gon.time_display_format = 0
gon.ee = Gitlab.ee?
gon.jh = Gitlab.jh?
gon.dot_com = Gitlab.com?
@@ -67,6 +68,7 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
gon.time_display_relative = current_user.time_display_relative
+ gon.time_display_format = current_user.time_display_format
end
# Initialize gon.features with any flags that should be
@@ -75,7 +77,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
- push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:custom_emoji)
diff --git a/lib/gitlab/graphql/tracers/timer_tracer.rb b/lib/gitlab/graphql/tracers/timer_tracer.rb
index 8e058621110..2cf06086a3c 100644
--- a/lib/gitlab/graphql/tracers/timer_tracer.rb
+++ b/lib/gitlab/graphql/tracers/timer_tracer.rb
@@ -15,11 +15,11 @@ module Gitlab
end
def trace(key, data)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
yield
ensure
- data[:duration_s] = Gitlab::Metrics::System.monotonic_time - start_time
+ data[:duration_s] = ::Gitlab::Metrics::System.monotonic_time - start_time
end
end
end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 8ca88859b22..6fe7a0030f0 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -13,7 +13,7 @@ module Gitlab
# rubocop:disable CodeReuse/ActiveRecord
def users
groups = group.self_and_hierarchy_intersecting_with_user_groups(current_user)
- groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
+ groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/427108")
members = GroupMember.where(group: groups).non_invite
users = super
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 08d44184bb6..720f8748cba 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Detect user based on identifier like
+# Detect user or keys based on identifier like
# key-13 or user-36
module Gitlab
module Identifier
@@ -35,6 +35,13 @@ module Gitlab
end
end
+ # Tries to identify a deploy key using a SSH key identifier (e.g. "key-123").
+ def identify_using_deploy_key(identifier)
+ key_id = identifier.gsub("key-", "")
+
+ DeployKey.find_by_id(key_id)
+ end
+
def identify_with_cache(category, key)
if identification_cache[category].key?(key)
identification_cache[category][key]
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index ea91b01afdb..523df1f9d5e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -40,13 +40,12 @@ module Gitlab
cmd = %W[gzip #{filepath}]
cmd << "-#{options}" if options
- _, status = Gitlab::Popen.popen(cmd)
+ output, status = Gitlab::Popen.popen(cmd)
- if status == 0
- status
- else
- raise Gitlab::ImportExport::Error.file_compression_error
- end
+ return status if status == 0
+
+ message = cmd_error_message(output, status)
+ raise Gitlab::ImportExport::Error.file_compression_error(message)
end
def mkdir_p(path)
@@ -104,9 +103,7 @@ module Gitlab
return true if status == 0
- output = output&.strip
- message = "command exited with error code #{status}"
- message += ": #{output}" if output.present?
+ message = cmd_error_message(output, status)
if @shared.respond_to?(:error)
@shared.error(Gitlab::ImportExport::Error.new(message))
@@ -149,6 +146,12 @@ module Gitlab
FileUtils.remove_dir(dir)
raise
end
+
+ def cmd_error_message(output, status)
+ message = "Command exited with error code #{status}"
+ message << ": #{output.strip}" unless output.blank?
+ message
+ end
end
end
end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index fa179f584eb..9b8e6374b5a 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -14,8 +14,8 @@ module Gitlab
self.new('Unknown object type')
end
- def self.file_compression_error
- self.new('File compression/decompression failed')
+ def self.file_compression_error(error)
+ self.new(format('File compression or decompression failed. %{error}', error: error))
end
def self.incompatible_import_file_error
diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb
index 543fd25d883..0cb0eb32a23 100644
--- a/lib/gitlab/import_export/project/sample/date_calculator.rb
+++ b/lib/gitlab/import_export/project/sample/date_calculator.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def calculate_by_closest_date_to_average(date)
- return date unless closest_date_to_average && closest_date_to_average < Time.current
+ return date unless closest_date_to_average && closest_date_to_average.past?
date + (Time.current - closest_date_to_average).seconds
end
diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb
index e39bbb36680..88991495a10 100644
--- a/lib/gitlab/instrumentation/redis_base.rb
+++ b/lib/gitlab/instrumentation/redis_base.rb
@@ -90,7 +90,7 @@ module Gitlab
result = ::Gitlab::Instrumentation::RedisClusterValidator.validate(commands)
return true if result.nil?
- if !result[:valid] && !result[:allowed] && (Rails.env.development? || Rails.env.test?)
+ if !result[:valid] && !result[:allowed] && raise_cross_slot_validation_errors?
raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
@@ -189,6 +189,10 @@ module Gitlab
redirection_type, _, target_node_key = err_msg.split
{ redirection_type: redirection_type, target_node_key: target_node_key }
end
+
+ def raise_cross_slot_validation_errors?
+ Rails.env.development? || Rails.env.test?
+ end
end
end
end
diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb
index 20ba1ab82a7..5934204bd0f 100644
--- a/lib/gitlab/instrumentation/redis_interceptor.rb
+++ b/lib/gitlab/instrumentation/redis_interceptor.rb
@@ -31,7 +31,7 @@ module Gitlab
private
def instrument_call(commands, pipelined = false)
- start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
+ start = ::Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
instrumentation_class.instance_count_request(commands.size)
instrumentation_class.instance_count_pipelined_request(commands.size) if pipelined
@@ -50,7 +50,7 @@ module Gitlab
instrumentation_class.log_exception(ex)
raise ex
ensure
- duration = Gitlab::Metrics::System.monotonic_time - start
+ duration = ::Gitlab::Metrics::System.monotonic_time - start
unless exclude_from_apdex?(commands)
commands.each { instrumentation_class.instance_observe_duration(duration / commands.size) }
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 2a3c4db5ffa..49078a7ccd0 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -12,7 +12,6 @@ module Gitlab
def add_instrumentation_data(payload)
instrument_gitaly(payload)
- instrument_rugged(payload)
instrument_redis(payload)
instrument_elasticsearch(payload)
instrument_zoekt(payload)
@@ -40,15 +39,6 @@ module Gitlab
payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time
end
- def instrument_rugged(payload)
- rugged_calls = Gitlab::RuggedInstrumentation.query_count
-
- return if rugged_calls == 0
-
- payload[:rugged_calls] = rugged_calls
- payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time
- end
-
def instrument_redis(payload)
payload.merge! ::Gitlab::Instrumentation::Redis.payload
end
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
index 2790bc8ee24..e2e4ea75dbf 100644
--- a/lib/gitlab/internal_events.rb
+++ b/lib/gitlab/internal_events.rb
@@ -23,8 +23,6 @@ module Gitlab
private
def increase_total_counter(event_name)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
redis_counter_key =
Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric.redis_key(event_name)
Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) }
diff --git a/lib/gitlab/issues/rebalancing/state.rb b/lib/gitlab/issues/rebalancing/state.rb
index 12cc5f6e5dd..c60dac6f571 100644
--- a/lib/gitlab/issues/rebalancing/state.rb
+++ b/lib/gitlab/issues/rebalancing/state.rb
@@ -100,7 +100,7 @@ module Gitlab
def refresh_keys_expiration
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
pipeline.expire(issue_ids_key, REDIS_EXPIRY_TIME)
pipeline.expire(current_index_key, REDIS_EXPIRY_TIME)
pipeline.expire(current_project_key, REDIS_EXPIRY_TIME)
diff --git a/lib/gitlab/jira/http_client.rb b/lib/gitlab/jira/http_client.rb
index 7abfe8e38e8..2b8b01e2023 100644
--- a/lib/gitlab/jira/http_client.rb
+++ b/lib/gitlab/jira/http_client.rb
@@ -34,6 +34,17 @@ module Gitlab
request_params[:headers][:Cookie] = get_cookies if options[:use_cookies]
request_params[:base_uri] = uri.to_s
request_params.merge!(auth_params)
+ # Setting defaults here so we can also set `timeout` which prevents setting defaults in the HTTP gem's code
+ request_params[:open_timeout] = options[:open_timeout] || default_timeout_for(:open_timeout)
+ request_params[:read_timeout] = options[:read_timeout] || default_timeout_for(:read_timeout)
+ request_params[:write_timeout] = options[:write_timeout] || default_timeout_for(:write_timeout)
+ # Global timeout. Needs to be at least as high as the maximum defined in other timeouts
+ request_params[:timeout] = [
+ Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT,
+ request_params[:open_timeout],
+ request_params[:read_timeout],
+ request_params[:write_timeout]
+ ].max
result = Gitlab::HTTP.public_send(http_method, path, **request_params) # rubocop:disable GitlabSecurity/PublicSend
@authenticated = result.response.is_a?(Net::HTTPOK)
@@ -52,6 +63,10 @@ module Gitlab
private
+ def default_timeout_for(param)
+ Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS[param]
+ end
+
def auth_params
return {} unless @options[:username] && @options[:password]
diff --git a/lib/gitlab/jira/middleware.rb b/lib/gitlab/jira/middleware.rb
deleted file mode 100644
index 8a74729da49..00000000000
--- a/lib/gitlab/jira/middleware.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Jira
- class Middleware
- def self.jira_dvcs_connector?(env)
- env['HTTP_USER_AGENT']&.downcase&.start_with?('jira dvcs connector')
- end
-
- def initialize(app)
- @app = app
- end
-
- def call(env)
- if self.class.jira_dvcs_connector?(env)
- env['HTTP_AUTHORIZATION'] = env['HTTP_AUTHORIZATION']&.sub('token', 'Bearer')
- end
-
- @app.call(env)
- end
- end
- end
-end
diff --git a/lib/gitlab/jira_import/base_importer.rb b/lib/gitlab/jira_import/base_importer.rb
index 2b83f0492cb..04ef1a0ef68 100644
--- a/lib/gitlab/jira_import/base_importer.rb
+++ b/lib/gitlab/jira_import/base_importer.rb
@@ -5,7 +5,7 @@ module Gitlab
class BaseImporter
attr_reader :project, :client, :formatter, :jira_project_key, :running_import
- def initialize(project)
+ def initialize(project, client = nil)
Gitlab::JiraImport.validate_project_settings!(project)
@running_import = project.latest_jira_import
@@ -14,7 +14,7 @@ module Gitlab
raise Projects::ImportService::Error, _('Unable to find Jira project to import data from.') unless @jira_project_key
@project = project
- @client = project.jira_integration.client
+ @client = client || project.jira_integration.client
@formatter = Gitlab::ImportFormatter.new
end
diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb
index 458f7c3f470..54ececc4938 100644
--- a/lib/gitlab/jira_import/issues_importer.rb
+++ b/lib/gitlab/jira_import/issues_importer.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :imported_items_cache_key, :start_at, :job_waiter
- def initialize(project)
+ def initialize(project, client = nil)
super
# get cached start_at value, or zero if not cached yet
@start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id)
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index e53bfb40654..7b491b3e14d 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -19,9 +19,6 @@ module Gitlab
class JobWaiter
KEY_PREFIX = "gitlab:job_waiter"
- STARTED_METRIC = :gitlab_job_waiter_started_total
- TIMEOUTS_METRIC = :gitlab_job_waiter_timeouts_total
-
# This TTL needs to be long enough to allow whichever Sidekiq job calls
# JobWaiter#wait to reach BLPOP.
DEFAULT_TTL = 6.hours.to_i
@@ -48,16 +45,15 @@ module Gitlab
Gitlab::Redis::SharedState.with { |redis| redis.del(key) } if key?(key)
end
- attr_reader :key, :finished, :worker_label
+ attr_reader :key, :finished
attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
# key - The key of this waiter.
- def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}", worker_label: nil)
+ def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}")
@key = key
@jobs_remaining = jobs_remaining
@finished = []
- @worker_label = worker_label
end
# Waits for all the jobs to be completed.
@@ -67,7 +63,6 @@ module Gitlab
# long to process, or is never processed.
def wait(timeout = 10)
deadline = Time.now.utc + timeout
- increment_counter(STARTED_METRIC)
Gitlab::Redis::SharedState.with do |redis|
while jobs_remaining > 0
@@ -81,10 +76,7 @@ module Gitlab
list, jid = redis.blpop(key, timeout: seconds_left)
# timed out
- unless list && jid
- increment_counter(TIMEOUTS_METRIC)
- break
- end
+ break unless list && jid
@finished << jid
@jobs_remaining -= 1
@@ -93,20 +85,5 @@ module Gitlab
finished
end
-
- private
-
- def increment_counter(metric)
- return unless worker_label
-
- metrics[metric].increment(worker: worker_label)
- end
-
- def metrics
- @metrics ||= {
- STARTED_METRIC => Gitlab::Metrics.counter(STARTED_METRIC, 'JobWaiter attempts started'),
- TIMEOUTS_METRIC => Gitlab::Metrics.counter(TIMEOUTS_METRIC, 'JobWaiter attempts timed out')
- }
- end
end
end
diff --git a/lib/gitlab/kubernetes/kubeconfig/template.rb b/lib/gitlab/kubernetes/kubeconfig/template.rb
index d40b9ce117e..844472f9c8e 100644
--- a/lib/gitlab/kubernetes/kubeconfig/template.rb
+++ b/lib/gitlab/kubernetes/kubeconfig/template.rb
@@ -44,7 +44,7 @@ module Gitlab
)
end
kubeconfig_yaml[:clusters].each do |cluster|
- ca_pem = cluster.dig(:cluster, :'certificate-authority-data')&.yield_self do |data|
+ ca_pem = cluster.dig(:cluster, :'certificate-authority-data')&.then do |data|
Base64.strict_decode64(data)
end
diff --git a/lib/gitlab/legacy_http.rb b/lib/gitlab/legacy_http.rb
index f38b2819c15..cf6ab80d37f 100644
--- a/lib/gitlab/legacy_http.rb
+++ b/lib/gitlab/legacy_http.rb
@@ -35,8 +35,8 @@ module Gitlab
read_total_timeout = options.fetch(:timeout, Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT)
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
- start_time ||= Gitlab::Metrics::System.monotonic_time
- elapsed = Gitlab::Metrics::System.monotonic_time - start_time
+ start_time ||= ::Gitlab::Metrics::System.monotonic_time
+ elapsed = ::Gitlab::Metrics::System.monotonic_time - start_time
if elapsed > read_total_timeout
raise Gitlab::HTTP::ReadTotalTimeout, "Request timed out after #{elapsed} seconds"
diff --git a/lib/gitlab/memory/reporter.rb b/lib/gitlab/memory/reporter.rb
index db0fd24983b..8d32745ac34 100644
--- a/lib/gitlab/memory/reporter.rb
+++ b/lib/gitlab/memory/reporter.rb
@@ -26,13 +26,13 @@ module Gitlab
perf_report: report.name
))
- start_monotonic_time = Gitlab::Metrics::System.monotonic_time
- start_thread_cpu_time = Gitlab::Metrics::System.thread_cpu_time
+ start_monotonic_time = ::Gitlab::Metrics::System.monotonic_time
+ start_thread_cpu_time = ::Gitlab::Metrics::System.thread_cpu_time
report_file = store_report(report)
- cpu_s = Gitlab::Metrics::System.thread_cpu_duration(start_thread_cpu_time)
- duration_s = Gitlab::Metrics::System.monotonic_time - start_monotonic_time
+ cpu_s = ::Gitlab::Metrics::System.thread_cpu_duration(start_thread_cpu_time)
+ duration_s = ::Gitlab::Metrics::System.monotonic_time - start_monotonic_time
@logger.info(
log_labels(
diff --git a/lib/gitlab/memory/reports_uploader.rb b/lib/gitlab/memory/reports_uploader.rb
index 76c3e0862e2..17230414a6a 100644
--- a/lib/gitlab/memory/reports_uploader.rb
+++ b/lib/gitlab/memory/reports_uploader.rb
@@ -13,11 +13,11 @@ module Gitlab
def upload(path)
log_upload_requested(path)
- start_monotonic_time = Gitlab::Metrics::System.monotonic_time
+ start_monotonic_time = ::Gitlab::Metrics::System.monotonic_time
File.open(path.to_s) { |file| fog.put_object(gcs_bucket, File.basename(path), file) }
- duration_s = Gitlab::Metrics::System.monotonic_time - start_monotonic_time
+ duration_s = ::Gitlab::Metrics::System.monotonic_time - start_monotonic_time
log_upload_success(path, duration_s)
rescue StandardError, Errno::ENOENT => error
log_exception(error)
diff --git a/lib/gitlab/merge_requests/mergeability/check_result.rb b/lib/gitlab/merge_requests/mergeability/check_result.rb
index e18909d8f17..075a897478b 100644
--- a/lib/gitlab/merge_requests/mergeability/check_result.rb
+++ b/lib/gitlab/merge_requests/mergeability/check_result.rb
@@ -5,6 +5,7 @@ module Gitlab
class CheckResult
SUCCESS_STATUS = :success
FAILED_STATUS = :failed
+ INACTIVE_STATUS = :inactive
attr_reader :status, :payload
@@ -20,6 +21,10 @@ module Gitlab
new(status: FAILED_STATUS, payload: default_payload.merge(**payload))
end
+ def self.inactive(payload: {})
+ new(status: INACTIVE_STATUS, payload: default_payload.merge(**payload))
+ end
+
def self.from_hash(data)
new(
status: data.fetch(:status).to_sym,
diff --git a/lib/gitlab/metrics/exporter/metrics_middleware.rb b/lib/gitlab/metrics/exporter/metrics_middleware.rb
index 258b655229e..b80a8c503e8 100644
--- a/lib/gitlab/metrics/exporter/metrics_middleware.rb
+++ b/lib/gitlab/metrics/exporter/metrics_middleware.rb
@@ -9,10 +9,10 @@ module Gitlab
default_labels = {
pid: pid
}
- @requests_total = Gitlab::Metrics.counter(
+ @requests_total = ::Gitlab::Metrics.counter(
:exporter_http_requests_total, 'Total number of HTTP requests', default_labels
)
- @request_durations = Gitlab::Metrics.histogram(
+ @request_durations = ::Gitlab::Metrics.histogram(
:exporter_http_request_duration_seconds,
'HTTP request duration histogram (seconds)',
default_labels,
@@ -21,9 +21,9 @@ module Gitlab
end
def call(env)
- start = Gitlab::Metrics::System.monotonic_time
+ start = ::Gitlab::Metrics::System.monotonic_time
@app.call(env).tap do |response|
- duration = Gitlab::Metrics::System.monotonic_time - start
+ duration = ::Gitlab::Metrics::System.monotonic_time - start
labels = {
method: env['REQUEST_METHOD'].downcase,
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index d2336ec4bb2..5a0612be88e 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -141,7 +141,7 @@ module Gitlab
return empty_result unless has_basic_credentials?(request)
login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, request: request)
return empty_result unless auth_result.success?
return empty_result unless auth_result.can?(:access_git)
diff --git a/lib/gitlab/middleware/path_traversal_check.rb b/lib/gitlab/middleware/path_traversal_check.rb
index 79465f3cb30..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,17 +60,25 @@ module Gitlab
end
end
- def check(env, log_params)
- request = ::Rack::Request.new(env)
- 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[:fullpath] = fullpath
+ log_params[:method] = request.request_method
+ 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/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index 81ad7a7f9e1..0bcd5b1196a 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -29,6 +29,8 @@ module Gitlab
{
authorize_params: { gl_auth_type: 'login' }
}
+ when ->(provider_name) { AuthHelper.saml_providers.include?(provider_name.to_sym) }
+ { attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements }
else
{}
end
@@ -61,7 +63,7 @@ module Gitlab
provider_arguments.concat arguments
provider_arguments << defaults unless defaults.empty?
when Hash, GitlabSettings::Options
- hash_arguments = arguments.deep_symbolize_keys.deep_merge(defaults)
+ hash_arguments = merge_hash_defaults_and_args(defaults, arguments)
normalized = normalize_hash_arguments(hash_arguments)
# A Hash from the configuration will be passed as is.
@@ -80,6 +82,13 @@ module Gitlab
provider_arguments
end
+ def merge_hash_defaults_and_args(defaults, arguments)
+ return arguments.to_hash if defaults.empty?
+ return defaults.deep_merge(arguments.deep_symbolize_keys) if Feature.enabled?(:invert_omniauth_args_merging)
+
+ arguments.to_hash.deep_symbolize_keys.deep_merge(defaults)
+ end
+
def normalize_hash_arguments(args)
args.deep_symbolize_keys!
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 3c8ac55f70b..adc417f287c 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -7,7 +7,7 @@ module Gitlab
module_function
def retry_lock(subject, max_retries = MAX_RETRIES, name:, &block)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
retry_attempts = 0
# prevent scope override, see https://gitlab.com/gitlab-org/gitlab/-/issues/391186
@@ -39,7 +39,7 @@ module Gitlab
def log_optimistic_lock_retries(name:, retry_attempts:, start_time:)
return unless retry_attempts > 0
- elapsed_time = Gitlab::Metrics::System.monotonic_time - start_time
+ elapsed_time = ::Gitlab::Metrics::System.monotonic_time - start_time
retry_lock_logger.info(
message: "Optimistic Lock released with retries",
diff --git a/lib/gitlab/pages/deployment_update.rb b/lib/gitlab/pages/deployment_update.rb
index 6845f5d88ec..bf6ac3a056d 100644
--- a/lib/gitlab/pages/deployment_update.rb
+++ b/lib/gitlab/pages/deployment_update.rb
@@ -89,14 +89,10 @@ module Gitlab
project.actual_limits.pages_file_entries
end
+ # If a newer pipeline already build a PagesDeployment
def validate_outdated_sha
return if latest?
-
- # use pipeline_id in case the build is retried
- last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
-
- return unless last_deployed_pipeline_id
- return if last_deployed_pipeline_id <= build.pipeline_id
+ return if latest_pipeline_id <= build.pipeline_id
errors.add(:base, 'build SHA is outdated for this ref')
end
@@ -111,6 +107,13 @@ module Gitlab
def sha
build.sha
end
+
+ def latest_pipeline_id
+ project
+ .active_pages_deployments
+ .with_path_prefix(build.pages&.dig(:path_prefix))
+ .latest_pipeline_id
+ end
end
end
end
diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb
index 81dcc54ff35..9e8c0c530a9 100644
--- a/lib/gitlab/pagination/cursor_based_keyset.rb
+++ b/lib/gitlab/pagination/cursor_based_keyset.rb
@@ -34,8 +34,10 @@ module Gitlab
order_satisfied?(relation, cursor_based_request_context)
end
- def self.enforced_for_type?(relation)
- ENFORCED_TYPES.include?(relation.klass)
+ def self.enforced_for_type?(request_scope, relation)
+ enforced = ENFORCED_TYPES
+ enforced += [::Ci::Build] if ::Feature.enabled?(:enforce_ci_builds_pagination_limit, request_scope, type: :ops)
+ enforced.include?(relation.klass)
end
def self.order_satisfied?(relation, cursor_based_request_context)
diff --git a/lib/gitlab/patch/sidekiq_cron_poller.rb b/lib/gitlab/patch/sidekiq_cron_poller.rb
index c9eae2f899f..8f1fbf53161 100644
--- a/lib/gitlab/patch/sidekiq_cron_poller.rb
+++ b/lib/gitlab/patch/sidekiq_cron_poller.rb
@@ -7,7 +7,7 @@
require 'sidekiq/version'
require 'sidekiq/cron/version'
-if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7')
+if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.12')
raise 'New version of sidekiq detected, please remove or update this patch'
end
diff --git a/lib/gitlab/patch/sidekiq_scheduled_enq.rb b/lib/gitlab/patch/sidekiq_scheduled_enq.rb
index de0e8465f97..b5a40c19923 100644
--- a/lib/gitlab/patch/sidekiq_scheduled_enq.rb
+++ b/lib/gitlab/patch/sidekiq_scheduled_enq.rb
@@ -15,10 +15,8 @@ module Gitlab
# this portion swaps out Sidekiq.redis for Gitlab::Redis::Queues
Gitlab::Redis::Queues.with do |conn| # rubocop:disable Cop/RedisQueueUsage
sorted_sets.each do |sorted_set|
- # adds namespace if `super` polls with a non-namespaced Sidekiq.redis
- if Gitlab::Utils.to_boolean(ENV['SIDEKIQ_ENQUEUE_NON_NAMESPACED'])
- sorted_set = "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{sorted_set}" # rubocop:disable Cop/RedisQueueUsage
- end
+ # adds namespace since `super` polls with a non-namespaced Sidekiq.redis
+ sorted_set = "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{sorted_set}" # rubocop:disable Cop/RedisQueueUsage
while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s])) # rubocop:disable Gitlab/ModuleWithInstanceVariables, Lint/AssignmentInCondition
Sidekiq::Client.push(Sidekiq.load_json(job)) # rubocop:disable Cop/SidekiqApiUsage
@@ -28,7 +26,6 @@ module Gitlab
end
end
- # calls original enqueue_jobs which may or may not be namespaced depending on SIDEKIQ_ENQUEUE_NON_NAMESPACED
super
end
end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index c9ed4720e83..5f2084ce011 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -60,14 +60,14 @@ module Gitlab
ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/third-party-logos/dotnet.svg'),
ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
- ProjectTemplate.new('bridgetown', 'Pages/Bridgetown', _('Everything you need to create a GitLab Pages site using Bridgetown'), 'https://gitlab.com/gitlab-org/project-templates/bridgetown'),
+ ProjectTemplate.new('bridgetown', 'Pages/Bridgetown', _('Everything you need to create a GitLab Pages site using Bridgetown'), 'https://gitlab.com/pages/bridgetown'),
ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby'), 'https://gitlab.com/pages/gatsby', 'illustrations/third-party-logos/gatsby.svg'),
ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
ProjectTemplate.new('pelican', 'Pages/Pelican', _('Everything you need to create a GitLab Pages site using Pelican'), 'https://gitlab.com/pages/pelican', 'illustrations/third-party-logos/pelican.svg'),
ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML'), 'https://gitlab.com/pages/plain-html'),
ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
- ProjectTemplate.new('middleman', 'Pages/Middleman', _('Everything you need to create a GitLab Pages site using Middleman'), 'https://gitlab.com/gitlab-org/project-templates/middleman', 'illustrations/logos/middleman.svg'),
+ ProjectTemplate.new('middleman', 'Pages/Middleman', _('Everything you need to create a GitLab Pages site using Middleman'), 'https://gitlab.com/pages/middleman', 'illustrations/logos/middleman.svg'),
ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'),
ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
@@ -81,7 +81,8 @@ module Gitlab
ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management'),
ProjectTemplate.new('kotlin_native_linux', 'Kotlin Native Linux', _('A basic template for developing Linux programs using Kotlin Native'), 'https://gitlab.com/gitlab-org/project-templates/kotlin-native-linux'),
ProjectTemplate.new('typo3_distribution', 'TYPO3 Distribution', _('A template for starting a new TYPO3 project'), 'https://gitlab.com/gitlab-org/project-templates/typo3-distribution', 'illustrations/logos/typo3.svg'),
- ProjectTemplate.new('laravel', 'Laravel Framework', _('A basic folder structure of a Laravel application, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/laravel', 'illustrations/logos/laravel.svg')
+ ProjectTemplate.new('laravel', 'Laravel Framework', _('A basic folder structure of a Laravel application, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/laravel', 'illustrations/logos/laravel.svg'),
+ ProjectTemplate.new('astro_tailwind', 'Astro Tailwind', _('A basic folder structure of Astro Starter Kit, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/astro-tailwind')
]
end
# rubocop:enable Metrics/AbcSize
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 4471d21b9ac..e817f2130f4 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -14,6 +14,7 @@ module Gitlab
:milestone,
:remove_source_branch,
:target,
+ :target_project,
:title,
:unassign,
:unlabel
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 9798b0eca2c..72bec159226 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -172,6 +172,25 @@ module Gitlab
end
end
+ desc { _('Request changes') }
+ explanation { _('Request changes to the current merge request.') }
+ types MergeRequest
+ condition do
+ Feature.enabled?(:mr_request_changes, current_user) &&
+ quick_action_target.persisted? &&
+ quick_action_target.find_reviewer(current_user)
+ end
+ command :request_changes do
+ result = ::MergeRequests::UpdateReviewerStateService.new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target, "requested_changes")
+
+ @execution_message[:request_changes] = if result[:status] == :success
+ _('Changes requested to the current merge request.')
+ else
+ result[:message]
+ end
+ end
+
desc { _('Approve a merge request') }
explanation { _('Approve the current merge request.') }
types MergeRequest
@@ -197,6 +216,10 @@ module Gitlab
next unless success
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target, "unreviewed")
+
@execution_message[:unapprove] = _('Unapproved the current merge request.')
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 89ec996488f..9f7599d2500 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -14,7 +14,6 @@ module Gitlab
Gitlab::Redis::FeatureFlag,
Gitlab::Redis::Queues,
Gitlab::Redis::QueuesMetadata,
- Gitlab::Redis::Pubsub,
Gitlab::Redis::RateLimiting,
Gitlab::Redis::RepositoryCache,
Gitlab::Redis::Sessions,
diff --git a/lib/gitlab/redis/cluster_util.rb b/lib/gitlab/redis/cluster_util.rb
index 5f1f39b5237..9e307940de3 100644
--- a/lib/gitlab/redis/cluster_util.rb
+++ b/lib/gitlab/redis/cluster_util.rb
@@ -26,6 +26,15 @@ module Gitlab
end
expired_count
end
+
+ # Redis cluster alternative to mget
+ def batch_get(keys, redis)
+ keys.each_slice(1000).flat_map do |subset|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+ subset.map { |key| pipeline.get(key) }
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index bbe5a8add4b..6acbf83df24 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -63,8 +63,12 @@ module Gitlab
hlen
hmget
hscan_each
+ llen
+ lrange
mapped_hmget
mget
+ pfcount
+ pttl
scan
scan_each
scard
@@ -72,20 +76,32 @@ module Gitlab
smembers
sscan
sscan_each
+ strlen
ttl
+ type
+ zcard
+ zcount
+ zrange
+ zrangebyscore
+ zrevrange
zscan_each
+ zscore
].freeze
WRITE_COMMANDS = %i[
+ decr
del
eval
expire
flushdb
hdel
+ hincrby
hset
incr
incrby
mapped_hmset
+ pfadd
+ pfmerge
publish
rpush
sadd
@@ -93,8 +109,15 @@ module Gitlab
set
setex
setnx
+ spop
srem
+ srem?
unlink
+ zadd
+ zpopmin
+ zrem
+ zremrangebyrank
+ zremrangebyscore
memory
].freeze
@@ -254,11 +277,27 @@ module Gitlab
#
# Let's define it explicitly instead of propagating it to method_missing
def close
- if use_primary_and_secondary_stores?
- [primary_store, secondary_store].map(&:close).first
+ if same_redis_store?
+ # if same_redis_store?, `use_primary_store_as_default?` returns false
+ # but we should avoid a feature-flag check in `.close` to avoid checking out
+ # an ActiveRecord connection during clean up.
+ secondary_store.close
else
- default_store.close
+ [primary_store, secondary_store].map(&:close).first
+ end
+ end
+
+ # blpop blocks until an element to be popped exist in the list or after a timeout.
+ def blpop(*args)
+ result = default_store.blpop(*args)
+ if !!result && use_primary_and_secondary_stores?
+ # special case to accommodate Gitlab::JobWaiter as blpop is only used in JobWaiter
+ # 1s should be sufficient wait time to account for delays between 1st and 2nd lpush
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2520#note_1630893702
+ non_default_store.blpop(args.first, timeout: 1)
end
+
+ result
end
private
@@ -380,7 +419,7 @@ module Gitlab
end
def redis_store?(store)
- store.is_a?(::Redis) || store.is_a?(::Redis::Namespace)
+ store.is_a?(::Redis)
end
def validate_stores!
diff --git a/lib/gitlab/redis/pubsub.rb b/lib/gitlab/redis/pubsub.rb
deleted file mode 100644
index b5022f467a2..00000000000
--- a/lib/gitlab/redis/pubsub.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Redis
- class Pubsub < ::Gitlab::Redis::Wrapper
- class << self
- def config_fallback
- SharedState
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index fb3a143121b..d12d3e8c6aa 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -3,6 +3,12 @@
module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
+ def self.redis
+ primary_store = ::Redis.new(ClusterSharedState.params)
+ secondary_store = ::Redis.new(params)
+
+ MultiStore.new(primary_store, secondary_store, store_name)
+ end
end
end
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 2bcf4769b5a..d5470bc0016 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -19,7 +19,7 @@ module Gitlab
InvalidPathError = Class.new(StandardError)
class << self
- delegate :params, :url, :store, to: :new
+ delegate :params, :url, :store, :encrypted_secrets, to: :new
def with
pool.with { |redis| yield redis }
@@ -110,6 +110,14 @@ module Gitlab
raw_config_hash[:sentinels]
end
+ def secret_file
+ if raw_config_hash[:secret_file].blank?
+ File.join(Settings.encrypted_settings['path'], 'redis.yaml.enc')
+ else
+ Settings.absolute(raw_config_hash[:secret_file])
+ end
+ end
+
def sentinels?
sentinels && !sentinels.empty?
end
@@ -118,22 +126,44 @@ module Gitlab
::Redis::Store::Factory.create(redis_store_options.merge(extras))
end
+ def encrypted_secrets
+ # In rake tasks, we have to populate the encrypted_secrets even if the
+ # file does not exist, as it is the job of one of those tasks to create
+ # the file. In other cases, like when being loaded as part of spinning
+ # up test environment via `scripts/setup-test-env`, we should gate on
+ # the presence of the specified secret file so that
+ # `Settings.encrypted`, which might not be loadable does not gets
+ # called.
+ Settings.encrypted(secret_file) if File.exist?(secret_file) || ::Gitlab::Runtime.rake?
+ end
+
private
def redis_store_options
config = raw_config_hash
config[:instrumentation_class] ||= self.class.instrumentation_class
- result = if config[:cluster].present?
- config[:db] = 0 # Redis Cluster only supports db 0
- config
+ decrypted_config = parse_encrypted_config(config)
+
+ result = if decrypted_config[:cluster].present?
+ decrypted_config[:db] = 0 # Redis Cluster only supports db 0
+ decrypted_config
else
- parse_redis_url(config)
+ parse_redis_url(decrypted_config)
end
parse_client_tls_options(result)
end
+ def parse_encrypted_config(encrypted_config)
+ encrypted_config.delete(:secret_file)
+
+ decrypted_secrets = encrypted_secrets&.config
+ encrypted_config.merge!(decrypted_secrets) if decrypted_secrets
+
+ encrypted_config
+ end
+
def parse_redis_url(config)
redis_url = config.delete(:url)
redis_uri = URI.parse(redis_url)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 2fd9dc9fa09..6ac37986d5c 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,6 +5,7 @@ module Gitlab
extend self
extend MergeRequests
extend Packages
+ extend Packages::Protection::Rules
def project_name_regex
# The character range \p{Alnum} overlaps with \u{00A9}-\u{1f9ff}
diff --git a/lib/gitlab/regex/packages.rb b/lib/gitlab/regex/packages.rb
index 6b178933a25..a0038d39318 100644
--- a/lib/gitlab/regex/packages.rb
+++ b/lib/gitlab/regex/packages.rb
@@ -3,6 +3,8 @@
module Gitlab
module Regex
module Packages
+ include ::Gitlab::Utils::StrongMemoize
+
CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
@@ -74,8 +76,10 @@ module Gitlab
maven_app_name_regex
end
- def npm_package_name_regex
- @npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z}o
+ def npm_package_name_regex(other_accepted_chars = nil)
+ strong_memoize_with(:npm_package_name_regex, other_accepted_chars) do
+ %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9#{other_accepted_chars}]+\z}
+ end
end
def npm_package_name_regex_message
diff --git a/lib/gitlab/regex/packages/protection/rules.rb b/lib/gitlab/regex/packages/protection/rules.rb
new file mode 100644
index 00000000000..383f26fe92d
--- /dev/null
+++ b/lib/gitlab/regex/packages/protection/rules.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Regex
+ module Packages
+ module Protection
+ module Rules
+ def protection_rules_npm_package_name_pattern_regex
+ @protection_rules_npm_package_name_pattern_regex ||= npm_package_name_regex('*')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index 3a389d3363f..d5e80053772 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -6,7 +6,8 @@
module Gitlab
module RequestForgeryProtection
- class Controller < BaseActionController
+ # rubocop:disable Rails/ApplicationController
+ class Controller < ActionController::Base
protect_from_forgery with: :exception, prepend: true
def initialize
@@ -39,5 +40,6 @@ module Gitlab
rescue ActionController::InvalidAuthenticityToken
false
end
+ # rubocop:enable Rails/ApplicationController
end
end
diff --git a/lib/gitlab/rugged_instrumentation.rb b/lib/gitlab/rugged_instrumentation.rb
deleted file mode 100644
index 36a3a491de6..00000000000
--- a/lib/gitlab/rugged_instrumentation.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module RuggedInstrumentation
- def self.query_time
- query_time = SafeRequestStore[:rugged_query_time] || 0
- query_time.round(Gitlab::InstrumentationHelper::DURATION_PRECISION)
- end
-
- def self.add_query_time(duration)
- SafeRequestStore[:rugged_query_time] ||= 0
- SafeRequestStore[:rugged_query_time] += duration
- end
-
- def self.query_time_ms
- (self.query_time * 1000).round(2)
- end
-
- def self.query_count
- SafeRequestStore[:rugged_call_count] ||= 0
- end
-
- def self.increment_query_count
- SafeRequestStore[:rugged_call_count] ||= 0
- SafeRequestStore[:rugged_call_count] += 1
- end
-
- def self.active?
- SafeRequestStore.active?
- end
-
- def self.add_call_details(details)
- return unless Gitlab::PerformanceBar.enabled_for_request?
-
- Gitlab::SafeRequestStore[:rugged_call_details] ||= []
- Gitlab::SafeRequestStore[:rugged_call_details] << details
- end
-
- def self.list_call_details
- return [] unless Gitlab::PerformanceBar.enabled_for_request?
-
- Gitlab::SafeRequestStore[:rugged_call_details] || []
- end
- end
-end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index d06f414bd9a..fada3b84401 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -191,9 +191,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
- if Feature.enabled?(:search_issues_hide_archived_projects, current_user) && !filters[:include_archived]
- project_ids = project_ids.non_archived
- end
+ project_ids = project_ids.non_archived unless filters[:include_archived]
issues = issues.in_projects(project_ids)
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/420046')
@@ -218,9 +216,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
- if Feature.enabled?(:search_merge_requests_hide_archived_projects, current_user) && !filters[:include_archived]
- project_ids = project_ids.non_archived
- end
+ project_ids = project_ids.non_archived unless filters[:include_archived]
merge_requests = merge_requests.of_projects(project_ids)
end
diff --git a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
new file mode 100644
index 00000000000..2971dabe044
--- /dev/null
+++ b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ module Catalog
+ class ResourceSeeder
+ # This is currently disabled until it gets fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+ # Initializes the class
+ #
+ # @param [String] Path of the group to find
+ # @param [Integer] Number of resources to create
+ def initialize(group_path:, seed_count:)
+ @group = Group.find_by_full_path(group_path)
+ @seed_count = seed_count
+ @current_user = @group&.first_owner
+ end
+
+ def seed
+ if @group.nil?
+ warn 'ERROR: Group was not found.'
+ return
+ end
+
+ @seed_count.times do |i|
+ create_ci_catalog_resource(i)
+ end
+ end
+
+ private
+
+ def create_project(name, index)
+ project = ::Projects::CreateService.new(
+ @current_user,
+ description: "This is Catalog resource ##{index}",
+ name: name,
+ namespace_id: @group.id,
+ path: name,
+ visibility_level: @group.visibility_level
+ ).execute
+
+ if project.saved?
+ project
+ else
+ warn project.errors.full_messages.to_sentence
+ nil
+ end
+ end
+
+ def create_template_yml(project)
+ template_content = <<~YAML
+ spec:
+ inputs:
+ stage:
+ default: test
+ ---
+ component-job:
+ script: echo job 1
+ stage: $[[ inputs.stage ]]
+ YAML
+
+ project.repository.create_file(
+ @current_user,
+ 'template.yml',
+ template_content,
+ message: 'Add template.yml',
+ branch_name: project.default_branch_or_main
+ )
+ end
+
+ def create_readme(project, index)
+ project.repository.create_file(
+ @current_user,
+ '/README.md',
+ "## Component stuff #{index}",
+ message: 'Add README.md',
+ branch_name: project.default_branch_or_main
+ )
+ end
+
+ def create_ci_catalog(project)
+ result = ::Ci::Catalog::Resources::CreateService.new(project, @current_user).execute
+ if result.success?
+ result.payload
+ else
+ warn "Project '#{project.name}' could not be converted to a Catalog resource"
+ nil
+ end
+ end
+
+ def create_ci_catalog_resource(index)
+ name = "ci_seed_resource_#{index}"
+
+ if Project.find_by_name(name).present?
+ warn "Project '#{name}' already exists!"
+ return
+ end
+
+ project = create_project(name, index)
+
+ return unless project
+
+ create_readme(project, index)
+ create_template_yml(project)
+
+ return unless create_ci_catalog(project)
+
+ warn "Project '#{name}' was saved successfully!"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index a1363e7b6b2..10a69acc037 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -21,6 +21,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
DEFAULT_DUPLICATE_KEY_TTL = 6.hours
+ SHORT_DUPLICATE_KEY_TTL = 10.minutes
DEFAULT_STRATEGY = :until_executing
STRATEGY_NONE = :none
@@ -134,7 +135,7 @@ module Gitlab
jid != existing_jid
end
- def set_deduplicated_flag!(expiry = duplicate_key_ttl)
+ def set_deduplicated_flag!
return unless reschedulable?
with_redis { |redis| redis.eval(DEDUPLICATED_SCRIPT, keys: [cookie_key]) }
@@ -173,7 +174,7 @@ module Gitlab
end
def duplicate_key_ttl
- options[:ttl] || DEFAULT_DUPLICATE_KEY_TTL
+ options[:ttl] || default_duplicate_key_ttl
end
private
@@ -182,6 +183,12 @@ module Gitlab
attr_reader :queue_name, :job
attr_writer :existing_jid
+ def default_duplicate_key_ttl
+ return SHORT_DUPLICATE_KEY_TTL if Feature.enabled?(:reduce_duplicate_job_key_ttl)
+
+ DEFAULT_DUPLICATE_KEY_TTL
+ end
+
def worker_klass
@worker_klass ||= worker_class_name.to_s.safe_constantize
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
index b065190f656..e7ce837de29 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
@@ -20,7 +20,7 @@ module Gitlab
if duplicate_job.idempotent?
duplicate_job.update_latest_wal_location!
- duplicate_job.set_deduplicated_flag!(expiry)
+ duplicate_job.set_deduplicated_flag!
Gitlab::SidekiqLogging::DeduplicationLogger.instance.deduplicated_log(
job, strategy_name, duplicate_job.options)
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
index a8b3683e09f..37a9ed37891 100644
--- a/lib/gitlab/sidekiq_middleware/server_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -25,11 +25,6 @@ module Gitlab
def metrics
metrics = {
- sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_redis_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent requests a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS),
- sidekiq_elasticsearch_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'),
sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'),
@@ -43,9 +38,24 @@ module Gitlab
metrics[:sidekiq_jobs_completion_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS)
metrics[:sidekiq_jobs_queue_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS)
metrics[:sidekiq_jobs_failed_total] = ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed')
+
+ # resource usage
+ metrics[:sidekiq_jobs_cpu_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_jobs_db_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_jobs_gitaly_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_redis_requests_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
+ metrics[:sidekiq_elasticsearch_requests_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS)
else
- # The sum metric is still used in GitLab.com for dashboards
+ # These metrics are used in GitLab.com dashboards
metrics[:sidekiq_jobs_completion_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_completion_seconds_sum, 'Total of seconds to complete Sidekiq job')
+ metrics[:sidekiq_jobs_completion_count] = ::Gitlab::Metrics.counter(:sidekiq_jobs_completion_count, 'Number of Sidekiq jobs completed')
+
+ # resource usage sums
+ metrics[:sidekiq_jobs_cpu_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_cpu_seconds_sum, 'Total seconds this Sidekiq job spent on the CPU')
+ metrics[:sidekiq_jobs_db_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_db_seconds_sum, 'Total seconds of database time to run Sidekiq job')
+ metrics[:sidekiq_jobs_gitaly_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_gitaly_seconds_sum, 'Total seconds Gitaly time to run Sidekiq job')
+ metrics[:sidekiq_redis_requests_duration_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_redis_requests_duration_seconds_sum, 'Total duration in seconds that a Sidekiq job spent in requests to a Redis server')
+ metrics[:sidekiq_elasticsearch_requests_duration_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_duration_seconds_sum, 'Total duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server')
end
metrics
@@ -89,8 +99,9 @@ module Gitlab
# in metrics and can use them in the `ThreadsSampler` for setting a label
Thread.current.name ||= Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME
- labels = create_labels(worker.class, queue, job)
- instrument(job, labels) do
+ @job = job
+ @labels = create_labels(worker.class, queue, job)
+ instrument do
yield
end
end
@@ -99,8 +110,8 @@ module Gitlab
attr_reader :metrics
- def instrument(job, labels)
- queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
+ def instrument
+ @queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
@metrics[:sidekiq_jobs_queue_duration_seconds]&.observe(labels, queue_duration) if queue_duration
@@ -114,43 +125,33 @@ module Gitlab
@metrics[:sidekiq_jobs_interrupted_total].increment(labels, 1)
end
- job_succeeded = false
+ @job_succeeded = false
monotonic_time_start = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_start = get_thread_cputime
begin
transaction = Gitlab::Metrics::BackgroundTransaction.new
transaction.run { yield }
- job_succeeded = true
+ @job_succeeded = true
ensure
monotonic_time_end = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_end = get_thread_cputime
- monotonic_time = monotonic_time_end - monotonic_time_start
- job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
+ @monotonic_time = monotonic_time_end - monotonic_time_start
+ @job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- # sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
@metrics[:sidekiq_running_jobs].increment(labels, -1)
- if Feature.enabled?(:emit_sidekiq_histogram_metrics, type: :ops)
- @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
- else
- # we don't need job_status label here
- @metrics[:sidekiq_jobs_completion_seconds_sum].increment(labels, monotonic_time)
- end
+ @instrumentation = job[:instrumentation] || {}
+
+ record_resource_usage_counters
# job_status: done, fail match the job_status attribute in structured logging
labels[:job_status] = job_succeeded ? "done" : "fail"
- instrumentation = job[:instrumentation] || {}
- @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
- @metrics[:sidekiq_jobs_completion_seconds]&.observe(labels, monotonic_time)
+ record_histograms
- @metrics[:sidekiq_jobs_db_seconds].observe(labels, ActiveRecord::LogSubscriber.runtime / 1000)
- @metrics[:sidekiq_jobs_gitaly_seconds].observe(labels, get_gitaly_time(instrumentation))
@metrics[:sidekiq_redis_requests_total].increment(labels, get_redis_calls(instrumentation))
- @metrics[:sidekiq_redis_requests_duration_seconds].observe(labels, get_redis_time(instrumentation))
@metrics[:sidekiq_elasticsearch_requests_total].increment(labels, get_elasticsearch_calls(instrumentation))
- @metrics[:sidekiq_elasticsearch_requests_duration_seconds].observe(labels, get_elasticsearch_time(instrumentation))
@metrics[:sidekiq_mem_total_bytes].set(labels, get_thread_memory_total_allocations(instrumentation))
with_load_balancing_settings(job) do |settings|
@@ -162,15 +163,50 @@ module Gitlab
@metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1)
end
- sli_labels = labels.slice(*SIDEKIQ_SLI_LABELS)
- Gitlab::Metrics::SidekiqSlis.record_execution_apdex(sli_labels, monotonic_time) if job_succeeded
- Gitlab::Metrics::SidekiqSlis.record_execution_error(sli_labels, !job_succeeded)
- Gitlab::Metrics::SidekiqSlis.record_queueing_apdex(sli_labels, queue_duration) if queue_duration
+ @sli_labels = labels.slice(*SIDEKIQ_SLI_LABELS)
+ record_execution_sli
+ record_queueing_sli
end
end
private
+ attr_reader :labels, :job, :queue_duration, :job_succeeded, :monotonic_time, :job_thread_cputime, :instrumentation, :sli_labels
+
+ def record_resource_usage_counters
+ if Feature.enabled?(:emit_sidekiq_histogram_metrics, type: :ops)
+ @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
+ else
+ @metrics[:sidekiq_jobs_completion_seconds_sum].increment(labels, monotonic_time)
+ @metrics[:sidekiq_jobs_completion_count].increment(labels, 1)
+ @metrics[:sidekiq_jobs_cpu_seconds_sum].increment(labels, job_thread_cputime)
+ @metrics[:sidekiq_jobs_db_seconds_sum].increment(labels, ActiveRecord::LogSubscriber.runtime / 1000)
+ @metrics[:sidekiq_jobs_gitaly_seconds_sum].increment(labels, get_gitaly_time(instrumentation))
+ @metrics[:sidekiq_redis_requests_duration_seconds_sum].increment(labels, get_redis_time(instrumentation))
+ @metrics[:sidekiq_elasticsearch_requests_duration_seconds_sum].increment(labels, get_elasticsearch_time(instrumentation))
+ end
+ end
+
+ def record_histograms
+ @metrics[:sidekiq_jobs_cpu_seconds]&.observe(labels, job_thread_cputime)
+
+ @metrics[:sidekiq_jobs_completion_seconds]&.observe(labels, monotonic_time)
+
+ @metrics[:sidekiq_jobs_db_seconds]&.observe(labels, ActiveRecord::LogSubscriber.runtime / 1000)
+ @metrics[:sidekiq_jobs_gitaly_seconds]&.observe(labels, get_gitaly_time(instrumentation))
+ @metrics[:sidekiq_redis_requests_duration_seconds]&.observe(labels, get_redis_time(instrumentation))
+ @metrics[:sidekiq_elasticsearch_requests_duration_seconds]&.observe(labels, get_elasticsearch_time(instrumentation))
+ end
+
+ def record_queueing_sli
+ Gitlab::Metrics::SidekiqSlis.record_queueing_apdex(sli_labels, queue_duration) if queue_duration
+ end
+
+ def record_execution_sli
+ Gitlab::Metrics::SidekiqSlis.record_execution_apdex(sli_labels, monotonic_time) if job_succeeded
+ Gitlab::Metrics::SidekiqSlis.record_execution_error(sli_labels, !job_succeeded)
+ end
+
def with_load_balancing_settings(job)
keys = %w[load_balancing_strategy worker_data_consistency]
return unless keys.all? { |k| job.key?(k) }
diff --git a/lib/gitlab/sidekiq_middleware/skip_jobs.rb b/lib/gitlab/sidekiq_middleware/skip_jobs.rb
index 34ad843e8ee..56b150116a3 100644
--- a/lib/gitlab/sidekiq_middleware/skip_jobs.rb
+++ b/lib/gitlab/sidekiq_middleware/skip_jobs.rb
@@ -80,13 +80,20 @@ module Gitlab
end
health_check_attrs = worker_class.database_health_check_attrs
- job_base_model = Gitlab::Database.schemas_to_base_models[health_check_attrs[:gitlab_schema]].first
+
+ tables, schema = health_check_attrs.values_at(:tables, :gitlab_schema)
+
+ if health_check_attrs[:block].respond_to?(:call)
+ schema, tables = health_check_attrs[:block].call(job['args'], schema, tables)
+ end
+
+ job_base_model = Gitlab::Database.schemas_to_base_models[schema].first
health_context = Gitlab::Database::HealthStatus::Context.new(
DatabaseHealthStatusChecker.new(job['jid'], worker_class.name),
job_base_model.connection,
- health_check_attrs[:tables],
- health_check_attrs[:gitlab_schema]
+ tables,
+ schema
)
Gitlab::Database::HealthStatus.evaluate(health_context).any?(&:stop?)
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 778d278146d..ae4aca7ff92 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -94,8 +94,17 @@ module Gitlab
keys = job_ids.map { |jid| key_for(jid) }
- with_redis { |redis| redis.mget(*keys) }
- .map { |result| !result.nil? }
+ status = with_redis do |redis|
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(keys, redis)
+ else
+ redis.mget(*keys)
+ end
+ end
+ end
+
+ status.map { |result| !result.nil? }
end
# Returns the JIDs that are completed
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index f127e14243c..3bbcd59f45e 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module Tracking
class << self
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 1b7dcaa5cf4..a9b8dc313d0 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -40,6 +40,8 @@ module Gitlab
note_url(object, **options)
when Release
instance.release_url(object, **options)
+ when Organizations::Organization
+ instance.organization_url(object, **options)
when Project
instance.project_url(object, **options)
when Snippet
diff --git a/lib/gitlab/usage/metric_definition.rb b/lib/gitlab/usage/metric_definition.rb
index 7252283d1b9..941c2f793c4 100644
--- a/lib/gitlab/usage/metric_definition.rb
+++ b/lib/gitlab/usage/metric_definition.rb
@@ -3,7 +3,7 @@
module Gitlab
module Usage
class MetricDefinition
- METRIC_SCHEMA_PATH = Rails.root.join('config', 'metrics', 'schema.json')
+ METRIC_SCHEMA_PATH = Rails.root.join('config', 'metrics', 'schema', '**', '*.json')
AVAILABLE_STATUSES = %w[active broken].to_set.freeze
VALID_SERVICE_PING_STATUSES = %w[active broken].to_set.freeze
@@ -52,7 +52,7 @@ module Gitlab
end
def validate!
- self.class.schemer.validate(attributes.deep_stringify_keys).each do |error|
+ errors.each do |error|
error_message = <<~ERROR_MSG
Error type: #{error['type']}
Data: #{error['data']}
@@ -104,8 +104,10 @@ module Gitlab
definitions[key_path]&.to_context
end
- def schemer
- @schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
+ def schemers
+ @schemers ||= Dir[METRIC_SCHEMA_PATH].map do |path|
+ ::JSONSchemer.schema(Pathname.new(path))
+ end
end
def dump_metrics_yaml
@@ -145,6 +147,19 @@ module Gitlab
private
+ def errors
+ result = []
+
+ self.class.schemers.each do |schemer|
+ # schemer.validate returns an Enumerator object
+ schemer.validate(attributes.deep_stringify_keys).each do |error|
+ result << error
+ end
+ end
+
+ result
+ end
+
def method_missing(method, *args)
attributes[method] || super
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index b2027791e9d..5f819f060e4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -174,7 +174,6 @@ module Gitlab
prometheus_enabled: alt_usage_data(fallback: nil) { Gitlab::Prometheus::Internal.prometheus_enabled? },
prometheus_metrics_enabled: alt_usage_data(fallback: nil) { Gitlab::Metrics.prometheus_metrics_enabled? },
reply_by_email_enabled: alt_usage_data(fallback: nil) { Gitlab::Email::IncomingEmail.enabled? },
- web_ide_clientside_preview_enabled: alt_usage_data(fallback: nil) { false },
signup_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.allow_signup? },
grafana_link_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.grafana_enabled? },
gitpod_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.gitpod_enabled? }
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index f9dc8bd8a3c..185b49d4a68 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module UsageDataCounters
module HLLRedisCounter
@@ -53,8 +56,6 @@ module Gitlab
private
def track(values, event_name, time: Time.zone.now)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
event = event_for(event_name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownEvent.new("Unknown event #{event_name}")) unless event.present?
diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb
index 591e431c871..3f16681b642 100644
--- a/lib/gitlab/usage_data_counters/redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/redis_counter.rb
@@ -1,17 +1,16 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module UsageDataCounters
module RedisCounter
def increment(redis_counter_key)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) }
end
def increment_by(redis_counter_key, incr)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
Gitlab::Redis::SharedState.with { |redis| redis.incrby(redis_counter_key, incr) }
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f2db7e3c9b9..057e89a2a97 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -20,7 +20,7 @@ module Gitlab
include JwtAuthenticatable
class << self
- def git_http_ok(repository, repo_type, user, action, show_all_refs: false)
+ def git_http_ok(repository, repo_type, user, action, show_all_refs: false, need_audit: false)
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
attrs = {
@@ -28,6 +28,7 @@ module Gitlab
GL_REPOSITORY: repo_type.identifier_for_container(repository.container),
GL_USERNAME: user&.username,
ShowAllRefs: show_all_refs,
+ NeedAudit: need_audit,
Repository: repository.gitaly_repository.to_h,
GitConfigOptions: [],
GitalyServer: {
diff --git a/lib/peek/views/rugged.rb b/lib/peek/views/rugged.rb
deleted file mode 100644
index 3ed54a010f8..00000000000
--- a/lib/peek/views/rugged.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Peek
- module Views
- class Rugged < DetailedView
- def results
- return {} unless calls > 0
-
- super
- end
-
- private
-
- def duration
- ::Gitlab::RuggedInstrumentation.query_time_ms
- end
-
- def calls
- ::Gitlab::RuggedInstrumentation.query_count
- end
-
- def call_details
- ::Gitlab::RuggedInstrumentation.list_call_details
- end
-
- def format_call_details(call)
- super.merge(args: format_args(call[:args]))
- end
-
- def format_args(args)
- args.map do |arg|
- # ActiveSupport::JSON recursively calls as_json on all
- # instance variables, and if that instance variable points to
- # something that refers back to the same instance, we can wind
- # up in an infinite loop. Currently this only seems to happen with
- # Gitlab::Git::Repository and ::Repository.
- if arg.instance_variables.present?
- arg.to_s
- else
- arg
- end
- end
- end
- end
- end
-end
diff --git a/lib/sbom/purl_type/converter.rb b/lib/sbom/purl_type/converter.rb
index bfcfb414180..bc08083fdae 100644
--- a/lib/sbom/purl_type/converter.rb
+++ b/lib/sbom/purl_type/converter.rb
@@ -18,6 +18,7 @@ module Sbom
'nuget' => 'nuget',
'pip' => 'pypi',
'pipenv' => 'pypi',
+ 'poetry' => 'pypi',
'setuptools' => 'pypi',
'python-pkg' => 'pypi' # this package manager is generated by trivy
}.with_indifferent_access.freeze
diff --git a/lib/sidebars/admin/menus/admin_settings_menu.rb b/lib/sidebars/admin/menus/admin_settings_menu.rb
index 4656e0f33e2..4d2d19c60f7 100644
--- a/lib/sidebars/admin/menus/admin_settings_menu.rb
+++ b/lib/sidebars/admin/menus/admin_settings_menu.rb
@@ -12,7 +12,6 @@ module Sidebars
add_item(ci_cd_menu_item)
add_item(reporting_menu_item)
add_item(metrics_and_profiling_menu_item)
- add_item(service_usage_data_menu_item)
add_item(network_settings_menu_item)
add_item(appearance_menu_item)
add_item(preferences_menu_item)
@@ -102,15 +101,6 @@ module Sidebars
)
end
- def service_usage_data_menu_item
- ::Sidebars::MenuItem.new(
- title: _('Service usage data'),
- link: service_usage_data_admin_application_settings_path,
- active_routes: { path: 'admin/application_settings#service_usage_data' },
- item_id: :admin_service_usage
- )
- end
-
def network_settings_menu_item
::Sidebars::MenuItem.new(
title: _('Network'),
diff --git a/lib/sidebars/explore/menus/catalog_menu.rb b/lib/sidebars/explore/menus/catalog_menu.rb
new file mode 100644
index 00000000000..2d8e8bba08b
--- /dev/null
+++ b/lib/sidebars/explore/menus/catalog_menu.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Explore
+ module Menus
+ class CatalogMenu < ::Sidebars::Menu
+ override :link
+ def link
+ explore_catalog_index_path
+ end
+
+ override :title
+ def title
+ _('CI/CD Catalog')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'catalog-checkmark'
+ end
+
+ override :render?
+ def render?
+ Feature.enabled?(:global_ci_catalog, current_user)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: ['explore/catalog'] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/explore/panel.rb b/lib/sidebars/explore/panel.rb
index 6260df6bb5f..3559f7d9627 100644
--- a/lib/sidebars/explore/panel.rb
+++ b/lib/sidebars/explore/panel.rb
@@ -28,6 +28,7 @@ module Sidebars
def add_menus
add_menu(Sidebars::Explore::Menus::ProjectsMenu.new(context))
add_menu(Sidebars::Explore::Menus::GroupsMenu.new(context))
+ add_menu(Sidebars::Explore::Menus::CatalogMenu.new(context))
add_menu(Sidebars::Explore::Menus::TopicsMenu.new(context))
add_menu(Sidebars::Explore::Menus::SnippetsMenu.new(context))
end
diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb
index ee02429baf3..8fcb373c9dc 100644
--- a/lib/sidebars/menu.rb
+++ b/lib/sidebars/menu.rb
@@ -80,6 +80,7 @@ module Sidebars
is_active = @context.route_is_active.call(active_routes) || items.any? { |item| item[:is_active] }
{
+ id: self.class.name.demodulize.underscore,
title: title,
icon: sprite_icon,
avatar: avatar,
diff --git a/lib/sidebars/organizations/menus/manage_menu.rb b/lib/sidebars/organizations/menus/manage_menu.rb
index 0df716cdd3f..7c342002c31 100644
--- a/lib/sidebars/organizations/menus/manage_menu.rb
+++ b/lib/sidebars/organizations/menus/manage_menu.rb
@@ -30,6 +30,15 @@ module Sidebars
item_id: :organization_groups_and_projects
)
)
+ add_item(
+ ::Sidebars::MenuItem.new(
+ title: _('Users'),
+ link: users_organization_path(context.container),
+ super_sidebar_parent: ::Sidebars::Organizations::Menus::ManageMenu,
+ active_routes: { path: 'organizations/organizations#users' },
+ item_id: :organization_users
+ )
+ )
end
end
end
diff --git a/lib/sidebars/projects/menus/ci_cd_menu.rb b/lib/sidebars/projects/menus/ci_cd_menu.rb
index 02596b16cfa..c77e8e996b0 100644
--- a/lib/sidebars/projects/menus/ci_cd_menu.rb
+++ b/lib/sidebars/projects/menus/ci_cd_menu.rb
@@ -18,7 +18,7 @@ module Sidebars
override :extra_container_html_options
def extra_container_html_options
{
- class: 'shortcuts-pipelines rspec-link-pipelines'
+ class: 'shortcuts-pipelines'
}
end
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index b08845a37e6..d3c9f3a6466 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -70,7 +70,7 @@ module Sidebars
highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: callouts_path,
- auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
+ auto_devops_help_path: help_page_path('topics/autodevops/index') } }
end
def terraform_states_menu_item
diff --git a/lib/sidebars/projects/menus/scope_menu.rb b/lib/sidebars/projects/menus/scope_menu.rb
index f388c814bd7..d03abfdfb7e 100644
--- a/lib/sidebars/projects/menus/scope_menu.rb
+++ b/lib/sidebars/projects/menus/scope_menu.rb
@@ -22,7 +22,7 @@ module Sidebars
override :extra_container_html_options
def extra_container_html_options
{
- class: 'shortcuts-project rspec-project-link'
+ class: 'shortcuts-project'
}
end
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index 8fed1c46425..077eebf58b9 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -57,10 +57,6 @@ module Sidebars
monitor_menu_item,
usage_quotas_menu_item
]
- elsif context.current_user && can?(context.current_user, :manage_resource_access_tokens, context.project)
- [
- access_tokens_menu_item
- ]
else
[]
end
diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
index 0441d3b4a03..d4ecf132c44 100644
--- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
@@ -18,6 +18,7 @@ module Sidebars
def configure_menu_items
[
:tracing,
+ :metrics,
:error_tracking,
:alert_management,
:incidents,
diff --git a/lib/sidebars/user_settings/menus/comment_templates_menu.rb b/lib/sidebars/user_settings/menus/comment_templates_menu.rb
index da37c42bbd4..1e9aea8ec9a 100644
--- a/lib/sidebars/user_settings/menus/comment_templates_menu.rb
+++ b/lib/sidebars/user_settings/menus/comment_templates_menu.rb
@@ -23,7 +23,7 @@ module Sidebars
override :render?
def render?
- !!context.current_user && saved_replies_enabled?
+ !!context.current_user
end
override :active_routes
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index 240b808baf3..917fce42762 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -3,7 +3,7 @@
namespace :gitlab do
namespace :import do
desc "GitLab | Import | Add all users to all projects (admin users are added as maintainers)"
- task all_users_to_all_projects: :environment do |t, args|
+ task all_users_to_all_projects: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
projects = Project.all
diff --git a/lib/tasks/gitlab/click_house/migration.rake b/lib/tasks/gitlab/click_house/migration.rake
new file mode 100644
index 00000000000..ddac81ec98f
--- /dev/null
+++ b/lib/tasks/gitlab/click_house/migration.rake
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :clickhouse do
+ task :prepare_schema_migration_table, [:database] => :environment do |_t, args|
+ require_relative '../../../../lib/click_house/migration_support/schema_migration'
+
+ ClickHouse::MigrationSupport::SchemaMigration.create_table(args.database&.to_sym || :main)
+ end
+
+ desc 'GitLab | ClickHouse | Migrate'
+ task migrate: [:prepare_schema_migration_table] do
+ migrate(:up)
+ end
+
+ desc 'GitLab | ClickHouse | Rollback'
+ task rollback: [:prepare_schema_migration_table] do
+ migrate(:down)
+ end
+
+ private
+
+ def check_target_version
+ return unless target_version
+
+ version = ENV['VERSION']
+
+ return if ClickHouse::Migration::MIGRATION_FILENAME_REGEXP.match?(version) || /\A\d+\z/.match?(version)
+
+ raise "Invalid format of target version: `VERSION=#{version}`"
+ end
+
+ def target_version
+ ENV['VERSION'].to_i if ENV['VERSION'] && !ENV['VERSION'].empty?
+ end
+
+ def migrate(direction)
+ require_relative '../../../../lib/click_house/migration_support/schema_migration'
+ require_relative '../../../../lib/click_house/migration_support/migration_context'
+ require_relative '../../../../lib/click_house/migration_support/migrator'
+
+ check_target_version
+
+ scope = ENV['SCOPE']
+ verbose_was = ClickHouse::Migration.verbose
+ ClickHouse::Migration.verbose = ENV['VERBOSE'] ? ENV['VERBOSE'] != 'false' : true
+
+ migrations_paths = ClickHouse::MigrationSupport::Migrator.migrations_paths
+ schema_migration = ClickHouse::MigrationSupport::SchemaMigration
+ migration_context = ClickHouse::MigrationSupport::MigrationContext.new(migrations_paths, schema_migration)
+ migrations_ran = migration_context.public_send(direction, target_version) do |migration|
+ scope.blank? || scope == migration.scope
+ end
+
+ puts('No migrations ran.') unless migrations_ran&.any?
+ ensure
+ ClickHouse::Migration.verbose = verbose_was
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index cf52a219e83..d89ab548419 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -107,7 +107,10 @@ namespace :gitlab do
end
end
- Rake::Task['db:seed_fu'].invoke if databases_loaded.present? && databases_loaded.all?
+ if databases_loaded.present? && databases_loaded.all?
+ Rake::Task["gitlab:db:lock_writes"].invoke
+ Rake::Task['db:seed_fu'].invoke
+ end
end
def configure_database(connection, database_name: nil)
@@ -454,7 +457,12 @@ namespace :gitlab do
ActiveRecord::Base.establish_connection(config) # rubocop: disable Database/EstablishConnection
Gitlab::Database.check_for_non_superuser
- Rake::Task['db:migrate'].invoke
+
+ if Rake::Task.task_defined?("db:migrate:#{db_config.name}")
+ Rake::Task["db:migrate:#{db_config.name}"].invoke
+ else
+ Rake::Task["db:migrate"].invoke
+ end
end
end
diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake
deleted file mode 100644
index e44328e0de1..00000000000
--- a/lib/tasks/gitlab/features.rake
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-namespace :gitlab do
- namespace :features do
- desc 'GitLab | Features | Enable direct Git access via Rugged for NFS'
- task enable_rugged: :environment do
- set_rugged_feature_flags(true)
- puts 'All Rugged feature flags were enabled.'
- end
-
- task disable_rugged: :environment do
- set_rugged_feature_flags(false)
- puts 'All Rugged feature flags were disabled.'
- end
-
- task unset_rugged: :environment do
- set_rugged_feature_flags(nil)
- puts 'All Rugged feature flags were unset.'
- end
- end
-
- def set_rugged_feature_flags(status)
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
- case status
- when nil
- Feature.remove(flag)
- when true
- Feature.enable(flag)
- when false
- Feature.disable(flag)
- end
- end
- end
-end
diff --git a/lib/tasks/gitlab/redis.rake b/lib/tasks/gitlab/redis.rake
new file mode 100644
index 00000000000..6983c5fc318
--- /dev/null
+++ b/lib/tasks/gitlab/redis.rake
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :redis do
+ namespace :secret do
+ desc "GitLab | Redis | Secret | Show Redis secret"
+ task :show, [:instance_name] => [:environment] do |_t, args|
+ Gitlab::EncryptedRedisCommand.show(args: args)
+ end
+
+ desc "GitLab | Redis | Secret | Edit Redis secret"
+ task :edit, [:instance_name] => [:environment] do |_t, args|
+ Gitlab::EncryptedRedisCommand.edit(args: args)
+ end
+
+ desc "GitLab | Redis | Secret | Write Redis secret"
+ task :write, [:instance_name] => [:environment] do |_t, args|
+ content = $stdin.tty? ? $stdin.gets : $stdin.read
+ Gitlab::EncryptedRedisCommand.write(content, args: args)
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/ci_catalog_resources.rake b/lib/tasks/gitlab/seed/ci_catalog_resources.rake
new file mode 100644
index 00000000000..1db995aa801
--- /dev/null
+++ b/lib/tasks/gitlab/seed/ci_catalog_resources.rake
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# This task should be enabled when the seeder gets fixed:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+#
+# Seed CI/CD catalog resources
+#
+# @param group_path - Group name under which to create the projects
+# @param seed_count - Total number of Catalog resources to create (default: 30)
+#
+# @example
+# bundle exec rake "gitlab:seed:ci_catalog_resources[root, 50]"
+#
+# namespace :gitlab do
+# namespace :seed do
+# desc 'Seed CI Catalog resources'
+# task :ci_catalog_resources,
+# [:group_path, :seed_count] => :gitlab_environment do |_t, args|
+# Gitlab::Seeders::Ci::Catalog::ResourceSeeder.new(
+# group_path: args.group_path,
+# seed_count: args.seed_count&.to_i
+# ).seed
+# puts "Task finished!"
+# end
+# end
+# end
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 495d7a339b8..de1401feb8a 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -26,32 +26,31 @@ namespace :tw do
CodeOwnerRule.new('Analytics Instrumentation', '@lciutacu'),
CodeOwnerRule.new('Anti-Abuse', '@phillipwells'),
CodeOwnerRule.new('Cloud Connector', '@jglassman1'),
- CodeOwnerRule.new('Authentication and Authorization', '@jglassman1'),
+ CodeOwnerRule.new('Authentication', '@jglassman1'),
+ CodeOwnerRule.new('Authorization', '@jglassman1'),
# CodeOwnerRule.new('Billing and Subscription Management', ''),
CodeOwnerRule.new('Code Creation', '@jglassman1'),
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
- CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
CodeOwnerRule.new('Contributor Experience', '@eread'),
CodeOwnerRule.new('Database', '@aqualls'),
CodeOwnerRule.new('DataOps', '@sselhorn'),
# CodeOwnerRule.new('Delivery', ''),
- CodeOwnerRule.new('Development', '@sselhorn'),
CodeOwnerRule.new('Distribution', '@axil'),
CodeOwnerRule.new('Distribution (Charts)', '@axil'),
CodeOwnerRule.new('Distribution (Omnibus)', '@eread'),
- CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Duo Chat', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('Editor Extensions', '@aqualls'),
+ CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Foundations', '@sselhorn'),
# CodeOwnerRule.new('Fulfillment Platform', ''),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
CodeOwnerRule.new('Gitaly', '@eread'),
- # CodeOwnerRule.new('GitLab Dedicated', ''),
+ CodeOwnerRule.new('GitLab Dedicated', '@lyspin'),
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
CodeOwnerRule.new('IDE', '@ashrafkhamis'),
CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'),
@@ -75,9 +74,9 @@ namespace :tw do
CodeOwnerRule.new('Runner', '@fneill'),
CodeOwnerRule.new('Runner SaaS', '@fneill'),
CodeOwnerRule.new('Security Policies', '@rdickenson'),
+ CodeOwnerRule.new('Solutions Architecture', '@jfullam @brianwald @Darwinjs'),
CodeOwnerRule.new('Source Code', '@msedlakjakubowski'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
- CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Threat Insights', '@rdickenson'),
@@ -87,6 +86,33 @@ namespace :tw do
# CodeOwnerRule.new('Vulnerability Research', '')
].freeze
+ CONTRIBUTOR_DOCS_PATH = '/doc/development/'
+ CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [
+ CodeOwnerRule.new('Analytics Instrumentation',
+ '@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \
+ '@gitlab-org/analytics-section/analytics-instrumentation/engineers'),
+ CodeOwnerRule.new('Authentication', '@gitlab-org/govern/authentication/approvers'),
+ CodeOwnerRule.new('Authorization', '@gitlab-org/govern/authorization/approvers'),
+ CodeOwnerRule.new('Compliance',
+ '@gitlab-org/govern/security-policies-frontend @gitlab-org/govern/threat-insights-frontend-team ' \
+ '@gitlab-org/govern/threat-insights-backend-team'),
+ CodeOwnerRule.new('Composition Analysis',
+ '@gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis'),
+ CodeOwnerRule.new('Distribution', '@gitlab-org/distribution'),
+ CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
+ CodeOwnerRule.new('Engineering Productivity', '@gl-quality/eng-prod'),
+ CodeOwnerRule.new('Foundations', '@gitlab-org/manage/foundations/engineering'),
+ CodeOwnerRule.new('Gitaly', '@proglottis @toon'),
+ CodeOwnerRule.new('Global Search', '@gitlab-org/search-team/migration-maintainers'),
+ CodeOwnerRule.new('IDE',
+ '@gitlab-org/maintainers/remote-development/backend @gitlab-org/maintainers/remote-development/frontend'),
+ CodeOwnerRule.new('Pipeline Authoring', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Pipeline Execution', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Product Analytics', '@gitlab-org/analytics-section/product-analytics/engineers/frontend'),
+ CodeOwnerRule.new('Tenant Scale', '@abdwdd @alexpooley @manojmj'),
+ CodeOwnerRule.new('Threat Insights', '@gitlab-org/govern/threat-insights-frontend-team')
+ ].freeze
+
ERRORS_EXCLUDED_FILES = [
'/doc/architecture'
].freeze
@@ -105,7 +131,8 @@ namespace :tw do
end
def self.writer_for_group(category, path)
- writer = CODE_OWNER_RULES.find { |rule| rule.category == category }&.writer
+ rules = path.start_with?(CONTRIBUTOR_DOCS_PATH) ? CONTRIBUTOR_DOCS_CODE_OWNER_RULES : CODE_OWNER_RULES
+ writer = rules.find { |rule| rule.category == category }&.writer
if writer.is_a?(String) || writer.nil?
writer
diff --git a/lib/tasks/tanuki_emoji.rake b/lib/tasks/tanuki_emoji.rake
index b3099853434..aec7d3c1bf6 100644
--- a/lib/tasks/tanuki_emoji.rake
+++ b/lib/tasks/tanuki_emoji.rake
@@ -30,9 +30,9 @@ namespace :tanuki_emoji do
require 'digest/sha2'
digest_emoji_map = {}
- emojis_map = {}
+ emojis_array = []
- TanukiEmoji.index.all.each do |emoji|
+ TanukiEmoji.index.all.sort_by(&:sort_key).each do |emoji|
emoji_path = Gitlab::Emoji.emoji_public_absolute_path.join("#{emoji.name}.png")
digest_entry = {
@@ -47,13 +47,14 @@ namespace :tanuki_emoji do
# Our new map is only characters to make the json substantially smaller
emoji_entry = {
+ n: emoji.name,
c: emoji.category,
e: emoji.codepoints,
d: emoji.description,
u: emoji.unicode_version
}
- emojis_map[emoji.name] = emoji_entry
+ emojis_array << emoji_entry
end
digests_json = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
@@ -63,7 +64,7 @@ namespace :tanuki_emoji do
emojis_json = Gitlab::Emoji.emoji_public_absolute_path.join('emojis.json')
File.open(emojis_json, 'w') do |handle|
- handle.write(Gitlab::Json.pretty_generate(emojis_map))
+ handle.write(Gitlab::Json.pretty_generate(emojis_array))
end
end
diff --git a/lib/unnested_in_filters/rewriter.rb b/lib/unnested_in_filters/rewriter.rb
index 9eb1c0b8273..2e334eb147b 100644
--- a/lib/unnested_in_filters/rewriter.rb
+++ b/lib/unnested_in_filters/rewriter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable CodeReuse/ActiveRecord (This module is generating ActiveRecord relations therefore using AR methods is necessary)
+# rubocop:disable CodeReuse/ActiveRecord -- This module is generating ActiveRecord relations therefore using AR methods is necessary
module UnnestedInFilters
class Rewriter
include Gitlab::Utils::StrongMemoize
@@ -295,3 +295,4 @@ module UnnestedInFilters
end
end
end
+# rubocop:enable CodeReuse/ActiveRecord
diff --git a/lib/vs_code/settings.rb b/lib/vs_code/settings.rb
index 30b91ebb16f..0cc2245eae1 100644
--- a/lib/vs_code/settings.rb
+++ b/lib/vs_code/settings.rb
@@ -15,7 +15,7 @@ module VsCode
}
]
}.freeze
- SETTINGS_TYPES = %w[settings extensions globalState machines keybindings snippets tasks].freeze
+ SETTINGS_TYPES = %w[settings extensions globalState machines keybindings snippets tasks profiles].freeze
DEFAULT_SESSION = "1"
NO_CONTENT_ETAG = "0"
end