diff options
42 files changed, 401 insertions, 137 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index af5e0ec93e0..008b62f6a0f 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -229,6 +229,9 @@ - "vendor/assets/**/*" - "{,ee/,jh/}{app/assets,app/helpers,app/presenters,app/views,locale,public,symbol}/**/*" +.controllers-patterns: &controllers-patterns + - "{,ee/,jh/}{app/controllers}/**/*" + .startup-css-patterns: &startup-css-patterns - "{,ee/,jh/}app/assets/stylesheets/startup/**/*" @@ -280,7 +283,7 @@ - ".dockerignore" - "qa/**/*" -# Code patterns + .ci-patterns + .workhorse-patterns +# Code patterns + .ci-patterns .code-patterns: &code-patterns - "{package.json,yarn.lock}" - ".browserslistrc" @@ -1614,11 +1617,13 @@ - <<: *if-dot-com-gitlab-org-merge-request changes: *frontend-patterns - <<: *if-dot-com-gitlab-org-merge-request + changes: *controllers-patterns + - <<: *if-dot-com-gitlab-org-merge-request + changes: *qa-patterns + - <<: *if-dot-com-gitlab-org-merge-request changes: *code-patterns when: manual allow_failure: true - - <<: *if-dot-com-gitlab-org-merge-request - changes: *qa-patterns - <<: *if-dot-com-gitlab-org-schedule variables: KNAPSACK_GENERATE_REPORT: "true" diff --git a/.rubocop_todo/rails/save_bang.yml b/.rubocop_todo/rails/save_bang.yml index 870e085eb28..807f2d6485d 100644 --- a/.rubocop_todo/rails/save_bang.yml +++ b/.rubocop_todo/rails/save_bang.yml @@ -50,9 +50,3 @@ Rails/SaveBang: - spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb - spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb - spec/lib/gitlab/import_export/uploads_manager_spec.rb - - spec/lib/gitlab/import_export/uploads_saver_spec.rb - - spec/lib/gitlab/import_export/wiki_restorer_spec.rb - - spec/lib/gitlab/lets_encrypt/client_spec.rb - - spec/lib/gitlab/middleware/go_spec.rb - - spec/lib/gitlab/shard_health_cache_spec.rb - - spec/mailers/notify_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d48b4ceb3..1c3d38f6810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 14.6.3 (2022-01-18) + +### Fixed (4 changes) + +- [Fix destruction of projects with pipelines](gitlab-org/gitlab@83e1616fe46b933c5b78b2d43e08463fdae4264a) ([merge request](gitlab-org/gitlab!78401)) +- [Geo: Resolve "undefined method each_batch"](gitlab-org/gitlab@a38bf23ebd0a9931ec5bb91377955824dcda39ea) ([merge request](gitlab-org/gitlab!78401)) **GitLab Enterprise Edition** +- [Fix migration for cases with empty strings](gitlab-org/gitlab@ddda8880db35b7d48ca8e4ec8efe54954d64f41f) ([merge request](gitlab-org/gitlab!78401)) +- [Geo: adapt verification timed out query to use state table](gitlab-org/gitlab@89212752226d6c5f34830e3f4a73c5a56764ed17) ([merge request](gitlab-org/gitlab!78401)) **GitLab Enterprise Edition** + ## 14.6.2 (2022-01-10) No changes. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 2ca34134182..f3a0458d512 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -b119c19734ba589a079381e8390952e37994149f +871f4415ed24c64e6a2746a4502f0bea38d0c440 diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 8b2d987ed90..809c245d2b9 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -112,6 +112,13 @@ module Ci from("(#{union_sql}) #{table_name}") } + scope :belonging_to_group_and_ancestors, -> (group_id) { + group_self_and_ancestors_ids = ::Group.find_by(id: group_id)&.self_and_ancestor_ids + + joins(:runner_namespaces) + .where(ci_runner_namespaces: { namespace_id: group_self_and_ancestors_ids }) + } + # deprecated # split this into: belonging_to_group & belonging_to_group_and_ancestors scope :legacy_belonging_to_group, -> (group_id, include_ancestors: false) { diff --git a/app/services/bulk_imports/archive_extraction_service.rb b/app/services/bulk_imports/archive_extraction_service.rb index 9fc828b8e34..caa40d98a76 100644 --- a/app/services/bulk_imports/archive_extraction_service.rb +++ b/app/services/bulk_imports/archive_extraction_service.rb @@ -28,8 +28,8 @@ module BulkImports end def execute - validate_filepath validate_tmpdir + validate_filepath validate_symlink extract_archive @@ -46,7 +46,7 @@ module BulkImports end def validate_tmpdir - raise(BulkImports::Error, 'Invalid target directory') unless File.expand_path(tmpdir).start_with?(Dir.tmpdir) + Gitlab::Utils.check_allowed_absolute_path!(tmpdir, [Dir.tmpdir]) end def validate_symlink diff --git a/app/services/bulk_imports/file_decompression_service.rb b/app/services/bulk_imports/file_decompression_service.rb index fe9017377ec..b76746b199f 100644 --- a/app/services/bulk_imports/file_decompression_service.rb +++ b/app/services/bulk_imports/file_decompression_service.rb @@ -1,21 +1,26 @@ # frozen_string_literal: true +# File Decompression Service allows gzipped files decompression into tmp directory. +# +# @param tmpdir [String] Temp directory to store downloaded file to. Must be located under `Dir.tmpdir`. +# @param filename [String] Name of the file to decompress. module BulkImports class FileDecompressionService include Gitlab::ImportExport::CommandLineUtil ServiceError = Class.new(StandardError) - def initialize(dir:, filename:) - @dir = dir + def initialize(tmpdir:, filename:) + @tmpdir = tmpdir @filename = filename - @filepath = File.join(@dir, @filename) + @filepath = File.join(@tmpdir, @filename) @decompressed_filename = File.basename(@filename, '.gz') - @decompressed_filepath = File.join(@dir, @decompressed_filename) + @decompressed_filepath = File.join(@tmpdir, @decompressed_filename) end def execute - validate_dir + validate_tmpdir + validate_filepath validate_decompressed_file_size if Feature.enabled?(:validate_import_decompressed_archive_size, default_enabled: :yaml) validate_symlink(filepath) @@ -33,10 +38,14 @@ module BulkImports private - attr_reader :dir, :filename, :filepath, :decompressed_filename, :decompressed_filepath + attr_reader :tmpdir, :filename, :filepath, :decompressed_filename, :decompressed_filepath - def validate_dir - raise(ServiceError, 'Invalid target directory') unless dir.start_with?(Dir.tmpdir) + def validate_filepath + Gitlab::Utils.check_path_traversal!(filepath) + end + + def validate_tmpdir + Gitlab::Utils.check_allowed_absolute_path!(tmpdir, [Dir.tmpdir]) end def validate_decompressed_file_size @@ -48,7 +57,7 @@ module BulkImports end def decompress_file - gunzip(dir: dir, filename: filename) + gunzip(dir: tmpdir, filename: filename) end def size_validator diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb index d08dc72e30b..8d6ba54cd50 100644 --- a/app/services/bulk_imports/file_download_service.rb +++ b/app/services/bulk_imports/file_download_service.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true -# Downloads a remote file. If no filename is given, it'll use the remote filename +# File Download Service allows remote file download into tmp directory. +# +# @param configuration [BulkImports::Configuration] Config object containing url and access token +# @param relative_url [String] Relative URL to download the file from +# @param tmpdir [String] Temp directory to store downloaded file to. Must be located under `Dir.tmpdir`. +# @param file_size_limit [Integer] Maximum allowed file size +# @param allowed_content_types [Array<String>] Allowed file content types +# @param filename [String] Name of the file to download, if known. Use remote filename if none given. module BulkImports class FileDownloadService ServiceError = Class.new(StandardError) @@ -13,20 +20,21 @@ module BulkImports def initialize( configuration:, relative_url:, - dir:, + tmpdir:, file_size_limit: DEFAULT_FILE_SIZE_LIMIT, allowed_content_types: DEFAULT_ALLOWED_CONTENT_TYPES, filename: nil) @configuration = configuration @relative_url = relative_url @filename = filename - @dir = dir + @tmpdir = tmpdir @file_size_limit = file_size_limit @allowed_content_types = allowed_content_types end def execute - validate_dir + validate_tmpdir + validate_filepath validate_url validate_content_type validate_content_length @@ -40,7 +48,7 @@ module BulkImports private - attr_reader :configuration, :relative_url, :dir, :file_size_limit, :allowed_content_types + attr_reader :configuration, :relative_url, :tmpdir, :file_size_limit, :allowed_content_types def download_file File.open(filepath, 'wb') do |file| @@ -76,8 +84,12 @@ module BulkImports @headers ||= http_client.head(relative_url).headers end - def validate_dir - raise(ServiceError, 'Invalid target directory') unless dir.start_with?(Dir.tmpdir) + def validate_filepath + Gitlab::Utils.check_path_traversal!(filepath) + end + + def validate_tmpdir + Gitlab::Utils.check_allowed_absolute_path!(tmpdir, [Dir.tmpdir]) end def validate_symlink @@ -119,7 +131,7 @@ module BulkImports end def filepath - @filepath ||= File.join(@dir, filename) + @filepath ||= File.join(@tmpdir, filename) end def filename diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 0bfdee088b4..13a77dbf2fd 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -22,41 +22,25 @@ .icon-container = sprite_icon('flag') - if @pipeline.child? - %span.js-pipeline-child.badge.badge-pill.gl-badge.sm.badge-primary.has-tooltip{ title: s_("Pipelines|This is a child pipeline within the parent pipeline") } - = s_('Pipelines|Child pipeline') - = surround '(', ')' do - = link_to s_('Pipelines|parent'), pipeline_path(@pipeline.triggered_by_pipeline), class: 'text-white text-underline' + - text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "<a href='#{pipeline_path(@pipeline.triggered_by_pipeline)}' class='text-underline'>", link_end: "</a>"}).html_safe + = gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") } - if @pipeline.latest? - %span.js-pipeline-url-latest.badge.badge-pill.gl-badge.sm.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") } - latest + = gl_badge_tag s_('Pipelines|latest'), { variant: :success, size: :sm }, { class: 'js-pipeline-url-latest has-tooltip', title: _("Latest pipeline for the most recent commit on this branch") } - if @pipeline.merge_train_pipeline? - %span.js-pipeline-url-train.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _("This is a merge train pipeline") } - train + = gl_badge_tag s_('Pipelines|train'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-train has-tooltip', title: _("This is a merge train pipeline") } - if @pipeline.has_yaml_errors? - %span.js-pipeline-url-yaml.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.yaml_errors } - yaml invalid + = gl_badge_tag s_('Pipelines|yaml invalid'), { variant: :danger, size: :sm }, { class: 'js-pipeline-url-yaml has-tooltip', title: @pipeline.yaml_errors } - if @pipeline.failure_reason? - %span.js-pipeline-url-failure.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.failure_reason } - error + = gl_badge_tag s_('Pipelines|error'), { variant: :danger, size: :sm }, { class: 'js-pipeline-url-failure has-tooltip', title: @pipeline.failure_reason } - if @pipeline.auto_devops_source? - popover_title_text = html_escape(_('This pipeline makes use of a predefined CI/CD configuration enabled by %{b_open}Auto DevOps.%{b_close}')) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } - popover_content_url = help_page_path('topics/autodevops/index.md') - popover_content_text = _('Learn more about Auto DevOps') - %a.js-pipeline-url-autodevops.badge.badge-pill.gl-badge.sm.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body", - toggle: "popover", - placement: "top", - html: "true", - triggers: "focus", - title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>", - content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>", - } } - Auto DevOps + = gl_badge_tag s_('Pipelines|Auto DevOps'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-autodevops', href: "#", tabindex: "0", role: "button", data: { container: 'body', toggle: 'popover', placement: 'top', html: 'true', triggers: 'focus', title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>", content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>" } } - if @pipeline.detached_merge_request_pipeline? - %span.js-pipeline-url-mergerequest.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') } - detached + = gl_badge_tag s_('Pipelines|detached'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') } - if @pipeline.stuck? - %span.js-pipeline-url-stuck.badge.badge-pill.gl-badge.sm.badge-warning - stuck + = gl_badge_tag s_('Pipelines|stuck'), { variant: :warning, size: :sm }, { class: 'js-pipeline-url-stuck has-tooltip' } .well-segment.branch-info .icon-container.commit-icon diff --git a/db/migrate/20220112205111_create_security_training_providers.rb b/db/migrate/20220112205111_create_security_training_providers.rb new file mode 100644 index 00000000000..afddec6a134 --- /dev/null +++ b/db/migrate/20220112205111_create_security_training_providers.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateSecurityTrainingProviders < Gitlab::Database::Migration[1.0] + def change + create_table :security_training_providers do |t| + t.text :name, limit: 256, null: false + t.text :description, limit: 512 + t.text :url, limit: 512, null: false + t.text :logo_url, limit: 512 + + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/migrate/20220113125401_create_security_trainings.rb b/db/migrate/20220113125401_create_security_trainings.rb new file mode 100644 index 00000000000..6924c7bd189 --- /dev/null +++ b/db/migrate/20220113125401_create_security_trainings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateSecurityTrainings < Gitlab::Database::Migration[1.0] + enable_lock_retries! + + def change + create_table :security_trainings do |t| + t.references :project, null: false, foreign_key: { on_delete: :cascade } + t.references :provider, null: false, foreign_key: { to_table: :security_training_providers, on_delete: :cascade } + t.boolean :is_primary, default: false, null: false + + t.timestamps_with_timezone null: false + + # Guarantee that there will be only one primary per project + t.index :project_id, name: 'index_security_trainings_on_unique_project_id', unique: true, where: 'is_primary IS TRUE' + end + end +end diff --git a/db/schema_migrations/20220112205111 b/db/schema_migrations/20220112205111 new file mode 100644 index 00000000000..a2d2d42271e --- /dev/null +++ b/db/schema_migrations/20220112205111 @@ -0,0 +1 @@ +65d9a1d63e90dfc336d0c69503c0899259fda773bc68a330782c206ac0fc48fd
\ No newline at end of file diff --git a/db/schema_migrations/20220113125401 b/db/schema_migrations/20220113125401 new file mode 100644 index 00000000000..7241e2e29c7 --- /dev/null +++ b/db/schema_migrations/20220113125401 @@ -0,0 +1 @@ +afe57b6b1b8b10e0e26d7f499b25adc5aef9f7c52af644d6a260f0ed3aab16d5
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1f8ff54765f..69910954a87 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19446,6 +19446,47 @@ CREATE SEQUENCE security_scans_id_seq ALTER SEQUENCE security_scans_id_seq OWNED BY security_scans.id; +CREATE TABLE security_training_providers ( + id bigint NOT NULL, + name text NOT NULL, + description text, + url text NOT NULL, + logo_url text, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_544b3dc935 CHECK ((char_length(url) <= 512)), + CONSTRAINT check_6fe222f071 CHECK ((char_length(logo_url) <= 512)), + CONSTRAINT check_a8ff21ced5 CHECK ((char_length(description) <= 512)), + CONSTRAINT check_dae433eed6 CHECK ((char_length(name) <= 256)) +); + +CREATE SEQUENCE security_training_providers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE security_training_providers_id_seq OWNED BY security_training_providers.id; + +CREATE TABLE security_trainings ( + id bigint NOT NULL, + project_id bigint NOT NULL, + provider_id bigint NOT NULL, + is_primary boolean DEFAULT false NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE security_trainings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE security_trainings_id_seq OWNED BY security_trainings.id; + CREATE TABLE self_managed_prometheus_alert_events ( id bigint NOT NULL, project_id bigint NOT NULL, @@ -22074,6 +22115,10 @@ ALTER TABLE ONLY security_orchestration_policy_rule_schedules ALTER COLUMN id SE ALTER TABLE ONLY security_scans ALTER COLUMN id SET DEFAULT nextval('security_scans_id_seq'::regclass); +ALTER TABLE ONLY security_training_providers ALTER COLUMN id SET DEFAULT nextval('security_training_providers_id_seq'::regclass); + +ALTER TABLE ONLY security_trainings ALTER COLUMN id SET DEFAULT nextval('security_trainings_id_seq'::regclass); + ALTER TABLE ONLY self_managed_prometheus_alert_events ALTER COLUMN id SET DEFAULT nextval('self_managed_prometheus_alert_events_id_seq'::regclass); ALTER TABLE ONLY sent_notifications ALTER COLUMN id SET DEFAULT nextval('sent_notifications_id_seq'::regclass); @@ -24015,6 +24060,12 @@ ALTER TABLE ONLY security_orchestration_policy_rule_schedules ALTER TABLE ONLY security_scans ADD CONSTRAINT security_scans_pkey PRIMARY KEY (id); +ALTER TABLE ONLY security_training_providers + ADD CONSTRAINT security_training_providers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY security_trainings + ADD CONSTRAINT security_trainings_pkey PRIMARY KEY (id); + ALTER TABLE ONLY self_managed_prometheus_alert_events ADD CONSTRAINT self_managed_prometheus_alert_events_pkey PRIMARY KEY (id); @@ -27486,6 +27537,12 @@ CREATE INDEX index_security_scans_on_pipeline_id ON security_scans USING btree ( CREATE INDEX index_security_scans_on_project_id ON security_scans USING btree (project_id); +CREATE INDEX index_security_trainings_on_project_id ON security_trainings USING btree (project_id); + +CREATE INDEX index_security_trainings_on_provider_id ON security_trainings USING btree (provider_id); + +CREATE UNIQUE INDEX index_security_trainings_on_unique_project_id ON security_trainings USING btree (project_id) WHERE (is_primary IS TRUE); + CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON self_managed_prometheus_alert_events USING btree (environment_id); CREATE INDEX index_sent_notifications_on_noteable_type_noteable_id ON sent_notifications USING btree (noteable_id) WHERE ((noteable_type)::text = 'Issue'::text); @@ -30753,6 +30810,9 @@ ALTER TABLE ONLY required_code_owners_sections ALTER TABLE ONLY dast_site_profiles ADD CONSTRAINT fk_rails_83e309d69e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY security_trainings + ADD CONSTRAINT fk_rails_84c7951d72 FOREIGN KEY (provider_id) REFERENCES security_training_providers(id) ON DELETE CASCADE; + ALTER TABLE ONLY zentao_tracker_data ADD CONSTRAINT fk_rails_84efda7be0 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE; @@ -31458,6 +31518,9 @@ ALTER TABLE ONLY internal_ids ALTER TABLE ONLY issues_self_managed_prometheus_alert_events ADD CONSTRAINT fk_rails_f7db2d72eb FOREIGN KEY (self_managed_prometheus_alert_event_id) REFERENCES self_managed_prometheus_alert_events(id) ON DELETE CASCADE; +ALTER TABLE ONLY security_trainings + ADD CONSTRAINT fk_rails_f80240fae0 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY merge_requests_closing_issues ADD CONSTRAINT fk_rails_f8540692be FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 49bfaf833f4..86d88215431 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14024,6 +14024,18 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="projectscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`. | +##### `Project.securityTrainingProviders` + +List of security training providers for the project. + +Returns [`[ProjectSecurityTraining!]`](#projectsecuritytraining). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectsecuritytrainingprovidersonlyenabled"></a>`onlyEnabled` | [`Boolean`](#boolean) | Filter the list by only enabled security trainings. | + ##### `Project.sentryDetailedError` Detailed version of a Sentry error on the project. @@ -14247,6 +14259,20 @@ Represents a Project Membership. | <a id="projectpermissionsupdatewiki"></a>`updateWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `update_wiki` on this resource. | | <a id="projectpermissionsuploadfile"></a>`uploadFile` | [`Boolean!`](#boolean) | Indicates the user can perform `upload_file` on this resource. | +### `ProjectSecurityTraining` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectsecuritytrainingdescription"></a>`description` | [`String`](#string) | Description of the training provider. | +| <a id="projectsecuritytrainingid"></a>`id` | [`GlobalID!`](#globalid) | ID of the training provider. | +| <a id="projectsecuritytrainingisenabled"></a>`isEnabled` | [`Boolean!`](#boolean) | Represents whether the provider is enabled or not. | +| <a id="projectsecuritytrainingisprimary"></a>`isPrimary` | [`Boolean!`](#boolean) | Represents whether the provider is set as primary or not. | +| <a id="projectsecuritytraininglogourl"></a>`logoUrl` | [`String`](#string) | Logo URL of the provider. | +| <a id="projectsecuritytrainingname"></a>`name` | [`String!`](#string) | Name of the training provider. | +| <a id="projectsecuritytrainingurl"></a>`url` | [`String!`](#string) | URL of the provider. | + ### `ProjectStatistics` #### Fields diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 23871bf3c68..a50c514733e 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # GitLab Licensing and Compatibility -[GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE) is licensed [under the terms of the MIT License](https://gitlab.com/gitlab-org/gitlab-foss/blob/master/LICENSE). [GitLab Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE) is licensed under "[The GitLab Enterprise Edition (EE) license](https://gitlab.com/gitlab-org/gitlab/-/blob/master/LICENSE)" wherein there are more restrictions. +[GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE) is licensed [under the terms of the MIT License](https://gitlab.com/gitlab-org/gitlab-foss/blob/master/LICENSE). [GitLab Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE) is licensed under "[The GitLab Enterprise Edition (EE) license](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/LICENSE)" wherein there are more restrictions. ## Automated Testing diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 4d8d0c5cdae..aef83109b9b 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -14,6 +14,7 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be - for merge requests with CI config changes - for merge requests with frontend changes +- for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*` - for merge requests with QA changes - for scheduled pipelines - the MR has the `pipeline:run-review-app` label set diff --git a/doc/user/admin_area/monitoring/background_migrations.md b/doc/user/admin_area/monitoring/background_migrations.md index 66001a987a4..260a8515a1a 100644 --- a/doc/user/admin_area/monitoring/background_migrations.md +++ b/doc/user/admin_area/monitoring/background_migrations.md @@ -145,6 +145,8 @@ or [manually finish it](#manually-finishing-a-batched-background-migration). ### Manually finishing a batched background migration +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62634) in GitLab 14.1 + If you need to manually finish a batched background migration due to an error, you can run: diff --git a/doc/user/group/value_stream_analytics/img/vsa_stage_table_v13_12.png b/doc/user/group/value_stream_analytics/img/vsa_stage_table_v13_12.png Binary files differdeleted file mode 100644 index 24d485306be..00000000000 --- a/doc/user/group/value_stream_analytics/img/vsa_stage_table_v13_12.png +++ /dev/null diff --git a/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png b/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png Binary files differnew file mode 100644 index 00000000000..c9074cbb5ea --- /dev/null +++ b/doc/user/group/value_stream_analytics/img/vsa_stage_table_v14_7.png diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md index 8e212afcb13..4663cfc8bfd 100644 --- a/doc/user/group/value_stream_analytics/index.md +++ b/doc/user/group/value_stream_analytics/index.md @@ -288,7 +288,7 @@ Shown metrics and charts includes: > Sorting the stage table [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/301082) in GitLab 13.12. -![Value stream analytics stage table](img/vsa_stage_table_v13_12.png "VSA stage table") +![Value stream analytics stage table](img/vsa_stage_table_v14_7.png "VSA stage table") The stage table shows a list of related workflow items for the selected stage. This can include: diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index f39c139b4fa..f21782a698f 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -229,7 +229,12 @@ module API use :pagination end get ':id/runners' do - runners = ::Ci::Runner.legacy_belonging_to_group(user_group.id, include_ancestors: true) + runners = if ::Feature.enabled?(:ci_find_runners_by_ci_mirrors, user_group, default_enabled: :yaml) + ::Ci::Runner.belonging_to_group_and_ancestors(user_group.id) + else + ::Ci::Runner.legacy_belonging_to_group(user_group.id, include_ancestors: true) + end + runners = apply_filter(runners, params) present paginate(runners), with: Entities::Ci::Runner diff --git a/lib/bulk_imports/common/extractors/ndjson_extractor.rb b/lib/bulk_imports/common/extractors/ndjson_extractor.rb index ecd7c08bd25..04febebff8e 100644 --- a/lib/bulk_imports/common/extractors/ndjson_extractor.rb +++ b/lib/bulk_imports/common/extractors/ndjson_extractor.rb @@ -4,49 +4,47 @@ module BulkImports module Common module Extractors class NdjsonExtractor - include Gitlab::ImportExport::CommandLineUtil - include Gitlab::Utils::StrongMemoize - def initialize(relation:) @relation = relation - @tmp_dir = Dir.mktmpdir + @tmpdir = Dir.mktmpdir end def extract(context) - download_service(tmp_dir, context).execute - decompression_service(tmp_dir).execute - relations = ndjson_reader(tmp_dir).consume_relation('', relation) + download_service(context).execute + decompression_service.execute + + records = ndjson_reader.consume_relation('', relation) - BulkImports::Pipeline::ExtractedData.new(data: relations) + BulkImports::Pipeline::ExtractedData.new(data: records) end - def remove_tmp_dir - FileUtils.remove_entry(tmp_dir) + def remove_tmpdir + FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir) end private - attr_reader :relation, :tmp_dir + attr_reader :relation, :tmpdir def filename - @filename ||= "#{relation}.ndjson.gz" + "#{relation}.ndjson.gz" end - def download_service(tmp_dir, context) + def download_service(context) @download_service ||= BulkImports::FileDownloadService.new( configuration: context.configuration, relative_url: context.entity.relation_download_url_path(relation), - dir: tmp_dir, + tmpdir: tmpdir, filename: filename ) end - def decompression_service(tmp_dir) - @decompression_service ||= BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: filename) + def decompression_service + @decompression_service ||= BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: filename) end - def ndjson_reader(tmp_dir) - @ndjson_reader ||= Gitlab::ImportExport::Json::NdjsonReader.new(tmp_dir) + def ndjson_reader + @ndjson_reader ||= Gitlab::ImportExport::Json::NdjsonReader.new(tmpdir) end end end diff --git a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb index 2ac4e533c1d..d7b9d6920ea 100644 --- a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb @@ -15,7 +15,7 @@ module BulkImports decompression_service.execute extraction_service.execute - upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*')) + upload_file_paths = Dir.glob(File.join(tmpdir, '**', '*')) BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths) end @@ -37,7 +37,7 @@ module BulkImports end def after_run(_) - FileUtils.remove_entry(tmp_dir) if Dir.exist?(tmp_dir) + FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir) end private @@ -46,17 +46,17 @@ module BulkImports BulkImports::FileDownloadService.new( configuration: context.configuration, relative_url: context.entity.relation_download_url_path(relation), - dir: tmp_dir, + tmpdir: tmpdir, filename: targz_filename ) end def decompression_service - BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: targz_filename) + BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename) end def extraction_service - BulkImports::ArchiveExtractionService.new(tmpdir: tmp_dir, filename: tar_filename) + BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename) end def relation @@ -71,8 +71,8 @@ module BulkImports "#{tar_filename}.gz" end - def tmp_dir - @tmp_dir ||= Dir.mktmpdir('bulk_imports') + def tmpdir + @tmpdir ||= Dir.mktmpdir('bulk_imports') end def file_uploader diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb index d5475a8b324..d85e51984df 100644 --- a/lib/bulk_imports/ndjson_pipeline.rb +++ b/lib/bulk_imports/ndjson_pipeline.rb @@ -68,7 +68,7 @@ module BulkImports end def after_run(_) - extractor.remove_tmp_dir if extractor.respond_to?(:remove_tmp_dir) + extractor.remove_tmpdir if extractor.respond_to?(:remove_tmpdir) end def relation_class(relation_key) diff --git a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb index 4d742225ff7..2492a023cbe 100644 --- a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb +++ b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb @@ -8,15 +8,16 @@ module BulkImports transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer - def extract(context) - download_service(tmp_dir, context).execute - decompression_service(tmp_dir).execute + def extract(_context) + download_service.execute + decompression_service.execute + project_attributes = json_decode(json_attributes) BulkImports::Pipeline::ExtractedData.new(data: project_attributes) end - def transform(_, data) + def transform(_context, data) subrelations = config.portable_relations_tree.keys.map(&:to_s) Gitlab::ImportExport::AttributeCleaner.clean( @@ -26,42 +27,42 @@ module BulkImports ).except(*subrelations) end - def load(_, data) + def load(_context, data) portable.assign_attributes(data) portable.reconcile_shared_runners_setting! portable.drop_visibility_level! portable.save! end - def after_run(_) - FileUtils.remove_entry(tmp_dir) + def after_run(_context) + FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir) end def json_attributes - @json_attributes ||= File.read(File.join(tmp_dir, filename)) + @json_attributes ||= File.read(File.join(tmpdir, filename)) end private - def tmp_dir - @tmp_dir ||= Dir.mktmpdir + def tmpdir + @tmpdir ||= Dir.mktmpdir('bulk_imports') end def config @config ||= BulkImports::FileTransfer.config_for(portable) end - def download_service(tmp_dir, context) + def download_service @download_service ||= BulkImports::FileDownloadService.new( configuration: context.configuration, - relative_url: context.entity.relation_download_url_path(BulkImports::FileTransfer::BaseConfig::SELF_RELATION), - dir: tmp_dir, + relative_url: context.entity.relation_download_url_path(BulkImports::FileTransfer::BaseConfig::SELF_RELATION), + tmpdir: tmpdir, filename: compressed_filename ) end - def decompression_service(tmp_dir) - @decompression_service ||= BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: compressed_filename) + def decompression_service + @decompression_service ||= BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: compressed_filename) end def compressed_filename diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index f4cb49c20fc..fb5d8cfa32f 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -462,6 +462,8 @@ security_findings: :gitlab_main security_orchestration_policy_configurations: :gitlab_main security_orchestration_policy_rule_schedules: :gitlab_main security_scans: :gitlab_main +security_training_providers: :gitlab_main +security_trainings: :gitlab_main self_managed_prometheus_alert_events: :gitlab_main sent_notifications: :gitlab_main sentry_issues: :gitlab_main diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ec4f91e39af..63cfffb0f2d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26149,6 +26149,9 @@ msgstr "" msgid "Pipelines|Are you sure you want to run this pipeline?" msgstr "" +msgid "Pipelines|Auto DevOps" +msgstr "" + msgid "Pipelines|Build with confidence" msgstr "" @@ -26161,7 +26164,7 @@ msgstr "" msgid "Pipelines|CI/CD template to test and deploy your %{name} project." msgstr "" -msgid "Pipelines|Child pipeline" +msgid "Pipelines|Child pipeline (%{link_start}parent%{link_end})" msgstr "" msgid "Pipelines|Clear runner caches" @@ -26326,10 +26329,25 @@ msgstr "" msgid "Pipelines|Visualize" msgstr "" +msgid "Pipelines|detached" +msgstr "" + +msgid "Pipelines|error" +msgstr "" + msgid "Pipelines|invalid" msgstr "" -msgid "Pipelines|parent" +msgid "Pipelines|latest" +msgstr "" + +msgid "Pipelines|stuck" +msgstr "" + +msgid "Pipelines|train" +msgstr "" + +msgid "Pipelines|yaml invalid" msgstr "" msgid "Pipeline|Actions" diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb index be8567ee0b6..c2bd61155b1 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb @@ -9,7 +9,7 @@ module QA expect(project_access_token.token).not_to be_nil project_access_token.revoke_via_ui! - expect(page).to have_text("Revoked project access token #{project_access_token.name}!") + expect(page).to have_text("Revoked access token #{project_access_token.name}!") end after do diff --git a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb index bd306233de8..d6e19a5fc85 100644 --- a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb +++ b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb @@ -16,7 +16,7 @@ RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do before do allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original - subject.instance_variable_set(:@tmp_dir, tmpdir) + subject.instance_variable_set(:@tmpdir, tmpdir) end after(:all) do @@ -43,11 +43,11 @@ RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do end end - describe '#remove_tmp_dir' do + describe '#remove_tmpdir' do it 'removes tmp dir' do expect(FileUtils).to receive(:remove_entry).with(tmpdir).once - subject.remove_tmp_dir + subject.remove_tmpdir end end end diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb index 3b5ea131d0d..9d43bb3ebfb 100644 --- a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb +++ b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do - let_it_be(:tmpdir) { Dir.mktmpdir } let_it_be(:project) { create(:project) } let_it_be(:group) { create(:group) } + let(:tmpdir) { Dir.mktmpdir } let(:uploads_dir_path) { File.join(tmpdir, '72a497a02fe3ee09edae2ed06d390038') } let(:upload_file_path) { File.join(uploads_dir_path, 'upload.txt')} let(:tracker) { create(:bulk_import_tracker, entity: entity) } @@ -80,10 +80,10 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do .with( configuration: context.configuration, relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=uploads", - dir: tmpdir, + tmpdir: tmpdir, filename: 'uploads.tar.gz') .and_return(download_service) - expect(BulkImports::FileDecompressionService).to receive(:new).with(dir: tmpdir, filename: 'uploads.tar.gz').and_return(decompression_service) + expect(BulkImports::FileDecompressionService).to receive(:new).with(tmpdir: tmpdir, filename: 'uploads.tar.gz').and_return(decompression_service) expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'uploads.tar').and_return(extraction_service) expect(download_service).to receive(:execute) @@ -123,6 +123,31 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do end end end + + describe '#after_run' do + before do + allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) + end + + it 'removes tmp dir' do + allow(FileUtils).to receive(:remove_entry).and_call_original + expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original + + pipeline.after_run(nil) + + expect(Dir.exist?(tmpdir)).to eq(false) + end + + context 'when dir does not exist' do + it 'does not attempt to remove tmpdir' do + FileUtils.remove_entry(tmpdir) + + expect(FileUtils).not_to receive(:remove_entry).with(tmpdir) + + pipeline.after_run(nil) + end + end + end end context 'when importing to group' do diff --git a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb index 11c475318bb..df7ff5b8062 100644 --- a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb @@ -56,7 +56,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do subject(:pipeline) { described_class.new(context) } before do - allow(Dir).to receive(:mktmpdir).and_return(tmpdir) + allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) end after do @@ -95,13 +95,13 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do .with( configuration: context.configuration, relative_url: "/#{entity.pluralized_name}/#{entity.source_full_path}/export_relations/download?relation=self", - dir: tmpdir, + tmpdir: tmpdir, filename: 'self.json.gz') .and_return(file_download_service) expect(BulkImports::FileDecompressionService) .to receive(:new) - .with(dir: tmpdir, filename: 'self.json.gz') + .with(tmpdir: tmpdir, filename: 'self.json.gz') .and_return(file_decompression_service) expect(file_download_service).to receive(:execute) @@ -156,4 +156,25 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do pipeline.json_attributes end end + + describe '#after_run' do + it 'removes tmp dir' do + allow(FileUtils).to receive(:remove_entry).and_call_original + expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original + + pipeline.after_run(nil) + + expect(Dir.exist?(tmpdir)).to eq(false) + end + + context 'when dir does not exist' do + it 'does not attempt to remove tmpdir' do + FileUtils.remove_entry(tmpdir) + + expect(FileUtils).not_to receive(:remove_entry).with(tmpdir) + + pipeline.after_run(nil) + end + end + end end diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 8e9be209f89..bfb18c58806 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::ImportExport::UploadsSaver do end it 'copies the uploads to the export path' do - saver.save + saver.save # rubocop:disable Rails/SaveBang uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) } @@ -54,7 +54,7 @@ RSpec.describe Gitlab::ImportExport::UploadsSaver do end it 'copies the uploads to the export path' do - saver.save + saver.save # rubocop:disable Rails/SaveBang uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) } diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb index f1284318687..1baf8749532 100644 --- a/spec/lib/gitlab/lets_encrypt/client_spec.rb +++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb @@ -42,7 +42,7 @@ RSpec.describe ::Gitlab::LetsEncrypt::Client do context 'when private key is saved in settings' do let!(:saved_private_key) do key = OpenSSL::PKey::RSA.new(4096).to_pem - Gitlab::CurrentSettings.current_application_settings.update(lets_encrypt_private_key: key) + Gitlab::CurrentSettings.current_application_settings.update!(lets_encrypt_private_key: key) key end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 1ef548ab29b..bc1d53b2ccb 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -100,7 +100,7 @@ RSpec.describe Gitlab::Middleware::Go do context 'without access to the project', :sidekiq_inline do before do - project.team.find_member(current_user).destroy + project.team.find_member(current_user).destroy! end it_behaves_like 'unauthorized' diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb index 5c47ac7e9a0..0c25cc7dab5 100644 --- a/spec/lib/gitlab/shard_health_cache_spec.rb +++ b/spec/lib/gitlab/shard_health_cache_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do let(:shards) { %w(foo bar) } before do - described_class.update(shards) + described_class.update(shards) # rubocop:disable Rails/SaveBang end describe '.clear' do @@ -24,7 +24,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do it 'replaces the existing set' do new_set = %w(test me more) - described_class.update(new_set) + described_class.update(new_set) # rubocop:disable Rails/SaveBang expect(described_class.cached_healthy_shards).to match_array(new_set) end @@ -36,7 +36,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do end it 'returns 0 if no shards are available' do - described_class.update([]) + described_class.update([]) # rubocop:disable Rails/SaveBang expect(described_class.healthy_shard_count).to eq(0) end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 44cb18008d2..0fbdc09a206 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -213,7 +213,7 @@ RSpec.describe Notify do subject { described_class.issue_due_email(recipient.id, issue.id) } before do - issue.update(due_date: Date.tomorrow) + issue.update!(due_date: Date.tomorrow) end it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -1229,7 +1229,7 @@ RSpec.describe Notify do end context 'when a comment on an existing discussion' do - let(:first_note) { create(model) } + let(:first_note) { create(model) } # rubocop:disable Rails/SaveBang let(:note) { create(model, author: note_author, noteable: nil, in_reply_to: first_note) } it 'contains an introduction' do @@ -1505,7 +1505,7 @@ RSpec.describe Notify do context 'member is not created by a user' do before do - group_member.update(created_by: nil) + group_member.update!(created_by: nil) end it_behaves_like 'no email is sent' @@ -1513,7 +1513,7 @@ RSpec.describe Notify do context 'member is a known user' do before do - group_member.update(user: create(:user)) + group_member.update!(user: create(:user)) end it_behaves_like 'no email is sent' @@ -1737,7 +1737,7 @@ RSpec.describe Notify do stub_config_setting(email_subject_suffix: 'A Nice Suffix') perform_enqueued_jobs do user.email = "new-email@mail.com" - user.save + user.save! end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 8f66978c311..6830a8daa3b 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1438,6 +1438,16 @@ RSpec.describe Ci::Runner do end end + describe '.belonging_to_group_and_ancestors' do + let_it_be(:parent_group) { create(:group) } + let_it_be(:parent_runner) { create(:ci_runner, :group, groups: [parent_group]) } + let_it_be(:group) { create(:group, parent: parent_group) } + + it 'returns the group runner from the parent group' do + expect(described_class.belonging_to_group_and_ancestors(group.id)).to contain_exactly(parent_runner) + end + end + describe '.belonging_to_group_or_project_descendants' do it 'returns the specific group runners' do group1 = create(:group) diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 6ca380a3cb9..305c0bd9df0 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -980,7 +980,7 @@ RSpec.describe API::Ci::Runners do end end - describe 'GET /groups/:id/runners' do + shared_context 'GET /groups/:id/runners' do context 'authorized user with maintainer privileges' do it 'returns all runners' do get api("/groups/#{group.id}/runners", user) @@ -1048,6 +1048,16 @@ RSpec.describe API::Ci::Runners do end end + it_behaves_like 'GET /groups/:id/runners' + + context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do + before do + stub_feature_flags(ci_find_runners_by_ci_mirrors: false) + end + + it_behaves_like 'GET /groups/:id/runners' + end + describe 'POST /projects/:id/runners' do context 'authorized user' do let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) } diff --git a/spec/services/bulk_imports/archive_extraction_service_spec.rb b/spec/services/bulk_imports/archive_extraction_service_spec.rb index aa823d88010..da9df31cde9 100644 --- a/spec/services/bulk_imports/archive_extraction_service_spec.rb +++ b/spec/services/bulk_imports/archive_extraction_service_spec.rb @@ -34,9 +34,9 @@ RSpec.describe BulkImports::ArchiveExtractionService do context 'when dir is not in tmpdir' do it 'raises an error' do - ['/etc', '/usr', '/', '/home', '', '/some/other/path', Rails.root].each do |path| + ['/etc', '/usr', '/', '/home', '/some/other/path', Rails.root.to_s].each do |path| expect { described_class.new(tmpdir: path, filename: 'filename').execute } - .to raise_error(BulkImports::Error, 'Invalid target directory') + .to raise_error(StandardError, "path #{path} is not allowed") end end end @@ -52,7 +52,7 @@ RSpec.describe BulkImports::ArchiveExtractionService do context 'when filepath is being traversed' do it 'raises an error' do - expect { described_class.new(tmpdir: File.join(tmpdir, '../../../'), filename: 'name').execute } + expect { described_class.new(tmpdir: File.join(Dir.mktmpdir, 'test', '..'), filename: 'name').execute } .to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path') end end diff --git a/spec/services/bulk_imports/file_decompression_service_spec.rb b/spec/services/bulk_imports/file_decompression_service_spec.rb index 4e8f78c8243..1d6aa79a37f 100644 --- a/spec/services/bulk_imports/file_decompression_service_spec.rb +++ b/spec/services/bulk_imports/file_decompression_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe BulkImports::FileDecompressionService do FileUtils.remove_entry(tmpdir) end - subject { described_class.new(dir: tmpdir, filename: gz_filename) } + subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) } describe '#execute' do it 'decompresses specified file' do @@ -55,10 +55,18 @@ RSpec.describe BulkImports::FileDecompressionService do end context 'when dir is not in tmpdir' do - subject { described_class.new(dir: '/etc', filename: 'filename') } + subject { described_class.new(tmpdir: '/etc', filename: 'filename') } it 'raises an error' do - expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid target directory') + expect { subject.execute }.to raise_error(StandardError, 'path /etc is not allowed') + end + end + + context 'when path is being traversed' do + subject { described_class.new(tmpdir: File.join(Dir.mktmpdir, 'test', '..'), filename: 'filename') } + + it 'raises an error' do + expect { subject.execute }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path') end end @@ -69,7 +77,7 @@ RSpec.describe BulkImports::FileDecompressionService do FileUtils.ln_s(File.join(tmpdir, gz_filename), symlink) end - subject { described_class.new(dir: tmpdir, filename: 'symlink.gz') } + subject { described_class.new(tmpdir: tmpdir, filename: 'symlink.gz') } it 'raises an error and removes the file' do expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file') @@ -87,7 +95,7 @@ RSpec.describe BulkImports::FileDecompressionService do subject.instance_variable_set(:@decompressed_filepath, symlink) end - subject { described_class.new(dir: tmpdir, filename: gz_filename) } + subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) } it 'raises an error and removes the file' do expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file') diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb index a24af9ae64d..bd664d6e996 100644 --- a/spec/services/bulk_imports/file_download_service_spec.rb +++ b/spec/services/bulk_imports/file_download_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe BulkImports::FileDownloadService do described_class.new( configuration: config, relative_url: '/test', - dir: tmpdir, + tmpdir: tmpdir, filename: filename, file_size_limit: file_size_limit, allowed_content_types: allowed_content_types @@ -72,7 +72,7 @@ RSpec.describe BulkImports::FileDownloadService do service = described_class.new( configuration: double, relative_url: '/test', - dir: tmpdir, + tmpdir: tmpdir, filename: filename, file_size_limit: file_size_limit, allowed_content_types: allowed_content_types @@ -157,7 +157,7 @@ RSpec.describe BulkImports::FileDownloadService do described_class.new( configuration: config, relative_url: '/test', - dir: tmpdir, + tmpdir: tmpdir, filename: 'symlink', file_size_limit: file_size_limit, allowed_content_types: allowed_content_types @@ -179,7 +179,7 @@ RSpec.describe BulkImports::FileDownloadService do described_class.new( configuration: config, relative_url: '/test', - dir: '/etc', + tmpdir: '/etc', filename: filename, file_size_limit: file_size_limit, allowed_content_types: allowed_content_types @@ -188,8 +188,28 @@ RSpec.describe BulkImports::FileDownloadService do it 'raises an error' do expect { subject.execute }.to raise_error( - described_class::ServiceError, - 'Invalid target directory' + StandardError, + 'path /etc is not allowed' + ) + end + end + + context 'when dir path is being traversed' do + subject do + described_class.new( + configuration: config, + relative_url: '/test', + tmpdir: File.join(Dir.mktmpdir('bulk_imports'), 'test', '..'), + filename: filename, + file_size_limit: file_size_limit, + allowed_content_types: allowed_content_types + ) + end + + it 'raises an error' do + expect { subject.execute }.to raise_error( + Gitlab::Utils::PathTraversalAttackError, + 'Invalid path' ) end end |