diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 78b5f3fb88b..6d6a5d2a813 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -329,7 +329,6 @@ linters: - 'ee/app/views/errors/kerberos_denied.html.haml' - 'ee/app/views/groups/ee/_settings_nav.html.haml' - 'ee/app/views/groups/group_members/_ldap_sync.html.haml' - - 'ee/app/views/groups/group_members/_sync_button.html.haml' - 'ee/app/views/groups/hooks/edit.html.haml' - 'ee/app/views/groups/ldap_group_links/index.html.haml' - 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml' @@ -362,7 +361,6 @@ linters: - 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml' - 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml' - 'ee/app/views/projects/settings/slacks/edit.html.haml' - - 'ee/app/views/shared/_mirror_update_button.html.haml' - 'ee/app/views/shared/epic/_search_bar.html.haml' - 'ee/app/views/shared/issuable/_approvals.html.haml' - 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d7df1264fe3..19115b7bedb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -186,31 +186,21 @@ RSpec/ExpectChange: # Offense count: 47 RSpec/ExpectGitlabTracking: Exclude: - - 'ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb' - 'ee/spec/controllers/projects/settings/operations_controller_spec.rb' - - 'ee/spec/controllers/registrations_controller_spec.rb' - 'ee/spec/requests/api/visual_review_discussions_spec.rb' - 'ee/spec/services/epics/issue_promote_service_spec.rb' - 'spec/controllers/groups/registry/repositories_controller_spec.rb' - - 'spec/controllers/groups_controller_spec.rb' - 'spec/controllers/projects/registry/repositories_controller_spec.rb' - 'spec/controllers/projects/registry/tags_controller_spec.rb' - 'spec/controllers/projects/settings/operations_controller_spec.rb' - 'spec/controllers/registrations_controller_spec.rb' - 'spec/lib/api/helpers_spec.rb' - - 'spec/lib/gitlab/experimentation_spec.rb' - - 'spec/mailers/notify_spec.rb' - - 'spec/models/project_services/prometheus_service_spec.rb' - 'spec/requests/api/project_container_repositories_spec.rb' - - 'spec/services/clusters/applications/check_installation_progress_service_spec.rb' - - 'spec/services/issues/zoom_link_service_spec.rb' - - 'spec/support/helpers/snowplow_helpers.rb' - 'spec/support/shared_examples/controllers/trackable_shared_examples.rb' - 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb' - 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb' - 'spec/support/shared_examples/requests/api/packages_shared_examples.rb' - 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb' - - 'spec/support/snowplow.rb' # Offense count: 751 RSpec/ExpectInHook: diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5059b30b278..4f781d9e61f 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -19109e7090eb6dab2b8c0c168bbef76ceca79a31 +0ebfb705b79a8baecc1db46f31761f83f4e471f9 diff --git a/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue b/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue index 9f75c65a316..fb80b158b46 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue @@ -1,6 +1,5 @@ @@ -75,6 +81,12 @@ export default { @secondary="onSecondary" @hide="() => $emit('hide')" > + import { isEmpty } from 'lodash'; -import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui'; +import { + GlIcon, + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlSprintf, + GlLink, + GlTooltipDirective, +} from '@gitlab/ui'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; import simplePoll from '~/lib/utils/simple_poll'; import { __ } from '~/locale'; @@ -36,6 +45,9 @@ export default { GlSprintf, GlLink, GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, MergeTrainHelperText: () => import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'), MergeImmediatelyConfirmationDialog: () => @@ -43,6 +55,9 @@ export default { 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue' ), }, + directives: { + GlTooltip: GlTooltipDirective, + }, mixins: [readyToMergeMixin], props: { mr: { type: Object, required: true }, @@ -283,7 +298,7 @@ export default { - + {{ mergeButtonText }} - - - - - - - - - {{ autoMergeText }} - - - - - - - - - {{ __('Merge immediately') }} - - - - - + + + {{ __('Select merge moment') }} + + + {{ __('Merge immediately') }} + + + + diff --git a/app/assets/javascripts/vue_shared/components/local_storage_sync.vue b/app/assets/javascripts/vue_shared/components/local_storage_sync.vue index 80c03342f11..33e77b6510c 100644 --- a/app/assets/javascripts/vue_shared/components/local_storage_sync.vue +++ b/app/assets/javascripts/vue_shared/components/local_storage_sync.vue @@ -22,11 +22,21 @@ export default { required: false, default: true, }, + clear: { + type: Boolean, + required: false, + default: false, + }, }, watch: { value(newVal) { this.saveValue(this.serialize(newVal)); }, + clear(newVal) { + if (newVal) { + localStorage.removeItem(this.storageKey); + } + }, }, mounted() { // On mount, trigger update if we actually have a localStorageValue diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index b5fd314698e..7b1953be69d 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -113,10 +113,6 @@ content: '\f0da'; } -.fa-refresh::before { - content: '\f021'; -} - .fa-chevron-up::before { content: '\f077'; } diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss index a74aeb9f220..581b7c37b5f 100644 --- a/app/assets/stylesheets/framework/spinner.scss +++ b/app/assets/stylesheets/framework/spinner.scss @@ -56,3 +56,7 @@ vertical-align: text-bottom; } } + +.spin { + animation: spinner-rotate 2s infinite linear; +} diff --git a/app/controllers/jwks_controller.rb b/app/controllers/jwks_controller.rb new file mode 100644 index 00000000000..e7b839f5590 --- /dev/null +++ b/app/controllers/jwks_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController + def index + render json: { keys: keys } + end + + private + + def keys + [ + # We keep openid_connect_signing_key so that we can seamlessly + # replace it with ci_jwt_signing_key and remove it on the next release. + # TODO: Remove openid_connect_signing_key in 13.7 + # https://gitlab.com/gitlab-org/gitlab/-/issues/221031 + Rails.application.secrets.openid_connect_signing_key, + Gitlab::CurrentSettings.ci_jwt_signing_key + ].compact.map do |key_data| + OpenSSL::PKey::RSA.new(key_data) + .public_key + .to_jwk + .slice(:kty, :kid, :e, :n) + .merge(use: 'sig', alg: 'RS256') + end + end +end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index c998de75ab2..28b1530456b 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -66,6 +66,11 @@ class MergeRequestsFinder < IssuableFinder by_source_project_id(items) end + def filter_negated_items(items) + items = super(items) + by_negated_target_branch(items) + end + private def by_commit(items) @@ -98,6 +103,14 @@ class MergeRequestsFinder < IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + def by_negated_target_branch(items) + return items unless not_params[:target_branch] + + items.where.not(target_branch: not_params[:target_branch]) + end + # rubocop: enable CodeReuse/ActiveRecord + def source_project_id @source_project_id ||= params[:source_project_id].presence end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 1b51f769f61..57e6fd4cb2c 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -74,6 +74,10 @@ module Types description: 'Time estimate of the issue' field :total_time_spent, GraphQL::INT_TYPE, null: false, description: 'Total time reported as spent on the issue' + field :human_time_estimate, GraphQL::STRING_TYPE, null: true, + description: 'Human-readable time estimate of the issue' + field :human_total_time_spent, GraphQL::STRING_TYPE, null: true, + description: 'Human-readable total time reported as spent on the issue' field :closed_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was closed' diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 6b27ab6a4e2..ae7882c0013 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -20,7 +20,8 @@ module SearchHelper resources_results = [ recent_items_autocomplete(term), groups_autocomplete(term), - projects_autocomplete(term) + projects_autocomplete(term), + issue_autocomplete(term) ].flatten search_pattern = Regexp.new(Regexp.escape(term), "i") @@ -183,6 +184,24 @@ module SearchHelper end # rubocop: enable CodeReuse/ActiveRecord + def issue_autocomplete(term) + return [] unless @project.present? && current_user && term =~ /\A#{Issue.reference_prefix}\d+\z/ + + iid = term.sub(Issue.reference_prefix, '').to_i + issue = @project.issues.find_by_iid(iid) + return [] unless issue && Ability.allowed?(current_user, :read_issue, issue) + + [ + { + category: 'In this project', + id: issue.id, + label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"), + url: issue_path(issue), + avatar_url: issue.project.avatar_url || '' + } + ] + end + # Autocomplete results for the current user's projects # rubocop: disable CodeReuse/ActiveRecord def projects_autocomplete(term, limit = 5) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index ad81e8f1c7d..55f3b51ac00 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -384,6 +384,9 @@ class ApplicationSetting < ApplicationRecord validates :raw_blob_request_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :ci_jwt_signing_key, + rsa_key: true, allow_nil: true + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -409,6 +412,7 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm + attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm before_validation :ensure_uuid! diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9ff70ece947..9c21db93a52 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1059,7 +1059,7 @@ module Ci jwt = Gitlab::Ci::Jwt.for_build(self) variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) - rescue OpenSSL::PKey::RSAError => e + rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e Gitlab::ErrorTracking.track_exception(e) end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4498e08d754..ee9c2501bfc 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -205,13 +205,8 @@ class CommitStatus < ApplicationRecord # 'rspec:linux: 1/10' => 'rspec:linux' common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '') - if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled? - # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' - common_name.gsub!(%r{: \[.*\]\s*\z}, '') - else - # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]' - common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '') - end + # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' + common_name.gsub!(%r{: \[.*\]\s*\z}, '') common_name.strip! common_name diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 34958936c9f..2bbcdbbe5ce 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -21,6 +21,7 @@ class GroupMember < Member scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } scope :of_ldap_type, -> { where(ldap: true) } scope :count_users_by_group_id, -> { group(:source_id).count } + scope :with_user, -> (user) { where(user: user) } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/validators/rsa_key_validator.rb b/app/validators/rsa_key_validator.rb new file mode 100644 index 00000000000..64595454a8c --- /dev/null +++ b/app/validators/rsa_key_validator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# RsaKeyValidator +# +# Custom validator for RSA private keys. +# +# class Project < ActiveRecord::Base +# validates :signing_key, rsa_key: true +# end +# +class RsaKeyValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_rsa_key?(value) + record.errors.add(attribute, "is not a valid RSA key") + end + end + + private + + def valid_rsa_key?(value) + return false unless value + + OpenSSL::PKey::RSA.new(value) + rescue OpenSSL::PKey::RSAError + false + end +end diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml index c558358725c..2cef6f97d48 100644 --- a/app/views/notify/_note_email.html.haml +++ b/app/views/notify/_note_email.html.haml @@ -9,7 +9,7 @@ = succeed ':' do = link_to note.author_name, user_url(note.author) - if discussion.nil? - commented + = link_to 'commented', target_url - else - if note.start_of_discussion? started a new diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index ba41243389b..5b074ff8a28 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -74,4 +74,4 @@ - if mirror.ssh_key_auth? = clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button') = render 'shared/remote_mirror_update_button', remote_mirror: mirror - %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') + %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index 54bd4ba04a0..70b72f74ab3 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -1,6 +1,6 @@ - if remote_mirror.update_in_progress? - %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') } - = icon("refresh spin") + %button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') } + = sprite_icon("retry", css_class: "spin") - elsif remote_mirror.enabled? - = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do - = icon("refresh") + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do + = sprite_icon("retry") diff --git a/changelogs/unreleased/214607-ci-jwt-signing-key-jwks-take-3.yml b/changelogs/unreleased/214607-ci-jwt-signing-key-jwks-take-3.yml new file mode 100644 index 00000000000..4900c4fa566 --- /dev/null +++ b/changelogs/unreleased/214607-ci-jwt-signing-key-jwks-take-3.yml @@ -0,0 +1,5 @@ +--- +title: Add CI JWT signing key to application_setings +merge_request: 43950 +author: +type: added diff --git a/changelogs/unreleased/233278-in-global-search-if-there-is-only-one-result-redirect-to-that-page.yml b/changelogs/unreleased/233278-in-global-search-if-there-is-only-one-result-redirect-to-that-page.yml new file mode 100644 index 00000000000..4610a56a4c9 --- /dev/null +++ b/changelogs/unreleased/233278-in-global-search-if-there-is-only-one-result-redirect-to-that-page.yml @@ -0,0 +1,5 @@ +--- +title: Search Autocomplete add GFM support for issues +merge_request: 44930 +author: +type: changed diff --git a/changelogs/unreleased/37102-expose-human-readable-time-estimate-via-graphql.yml b/changelogs/unreleased/37102-expose-human-readable-time-estimate-via-graphql.yml new file mode 100644 index 00000000000..a05278e94fc --- /dev/null +++ b/changelogs/unreleased/37102-expose-human-readable-time-estimate-via-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Expose humanTimeEstimate and humanTotalTimeSpent via graphql +merge_request: 45508 +author: +type: changed diff --git a/changelogs/unreleased/gy-fix-load-perf-docs.yml b/changelogs/unreleased/gy-fix-load-perf-docs.yml new file mode 100644 index 00000000000..0c3c32d3b26 --- /dev/null +++ b/changelogs/unreleased/gy-fix-load-perf-docs.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect code in Load Performance Testing docs +merge_request: 45877 +author: +type: other diff --git a/changelogs/unreleased/jduplessis_issue_comment_link.yml b/changelogs/unreleased/jduplessis_issue_comment_link.yml new file mode 100644 index 00000000000..2d896dfd57b --- /dev/null +++ b/changelogs/unreleased/jduplessis_issue_comment_link.yml @@ -0,0 +1,5 @@ +--- +title: Add link to the note on the email sent after adding a comment on an issue +merge_request: 45511 +author: +type: changed diff --git a/changelogs/unreleased/mw-replace-fa-refresh-icons.yml b/changelogs/unreleased/mw-replace-fa-refresh-icons.yml new file mode 100644 index 00000000000..3d4593cc223 --- /dev/null +++ b/changelogs/unreleased/mw-replace-fa-refresh-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-refresh icon with GitLab SVG +merge_request: 45777 +author: +type: changed diff --git a/changelogs/unreleased/ph-filterTargetBranchByNotEquals.yml b/changelogs/unreleased/ph-filterTargetBranchByNotEquals.yml new file mode 100644 index 00000000000..169bf356289 --- /dev/null +++ b/changelogs/unreleased/ph-filterTargetBranchByNotEquals.yml @@ -0,0 +1,5 @@ +--- +title: Fixed target branch not filtering +merge_request: 45652 +author: +type: fixed diff --git a/changelogs/unreleased/workaround_for_non_unique_project_fingerprints.yml b/changelogs/unreleased/workaround_for_non_unique_project_fingerprints.yml new file mode 100644 index 00000000000..4011fdad1ff --- /dev/null +++ b/changelogs/unreleased/workaround_for_non_unique_project_fingerprints.yml @@ -0,0 +1,5 @@ +--- +title: Add `position` column into security_findings table +merge_request: 44815 +author: +type: fixed diff --git a/config/feature_flags/development/ci_jwt_signing_key.yml b/config/feature_flags/development/ci_jwt_signing_key.yml new file mode 100644 index 00000000000..897cc06b47d --- /dev/null +++ b/config/feature_flags/development/ci_jwt_signing_key.yml @@ -0,0 +1,7 @@ +--- +name: ci_jwt_signing_key +introduced_by_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258546 +type: development +group: group::release management +default_enabled: false diff --git a/config/feature_flags/development/one_dimensional_matrix.yml b/config/feature_flags/development/one_dimensional_matrix.yml deleted file mode 100644 index 1db16474d38..00000000000 --- a/config/feature_flags/development/one_dimensional_matrix.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: one_dimensional_matrix -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42170 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/256062 -type: development -group: group::pipeline authoring -default_enabled: true diff --git a/config/routes.rb b/config/routes.rb index ef718c7618a..f6c8bb95eb3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -175,9 +175,8 @@ Rails.application.routes.draw do resources :abuse_reports, only: [:new, :create] # JWKS (JSON Web Key Set) endpoint - # Used by third parties to verify CI_JOB_JWT, placeholder route - # in case we decide to move away from doorkeeper-openid_connect - get 'jwks' => 'doorkeeper/openid_connect/discovery#keys' + # Used by third parties to verify CI_JOB_JWT + get 'jwks' => 'jwks#index' draw :snippets draw :profile diff --git a/db/fixtures/development/02_application_settings.rb b/db/fixtures/development/02_application_settings.rb index 67486d2ab5f..46da3205d8f 100644 --- a/db/fixtures/development/02_application_settings.rb +++ b/db/fixtures/development/02_application_settings.rb @@ -7,4 +7,7 @@ ApplicationSetting.create_from_defaults puts "Enable hashed storage for every new projects.".color(:green) ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) +puts "Generate CI JWT signing key".color(:green) +ApplicationSetting.current_without_cache.update!(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem) + print '.' diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index 7626cdb0b9c..65f70a9e715 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -24,3 +24,7 @@ if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? settings.prometheus_metrics_enabled = value save(settings, 'Prometheus metrics enabled flag') end + +settings = Gitlab::CurrentSettings.current_application_settings +settings.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(2048).to_pem +save(settings, 'CI JWT signing key') diff --git a/db/migrate/20201008011523_add_ci_jwt_signing_key_to_application_settings.rb b/db/migrate/20201008011523_add_ci_jwt_signing_key_to_application_settings.rb new file mode 100644 index 00000000000..c51f7e8b706 --- /dev/null +++ b/db/migrate/20201008011523_add_ci_jwt_signing_key_to_application_settings.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddCiJwtSigningKeyToApplicationSettings < ActiveRecord::Migration[6.0] + DOWNTIME = false + + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20201001011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv + def change + add_column :application_settings, :encrypted_ci_jwt_signing_key, :text + add_column :application_settings, :encrypted_ci_jwt_signing_key_iv, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20201008011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv.rb b/db/migrate/20201008011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv.rb new file mode 100644 index 00000000000..39f6eb2106a --- /dev/null +++ b/db/migrate/20201008011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddTextLimitToApplicationSettingsEncryptedCiJwtSigningKeyIv < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv, 255 + end + + def down + remove_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv + end +end diff --git a/db/migrate/20201008013434_generate_ci_jwt_signing_key.rb b/db/migrate/20201008013434_generate_ci_jwt_signing_key.rb new file mode 100644 index 00000000000..7983a56f439 --- /dev/null +++ b/db/migrate/20201008013434_generate_ci_jwt_signing_key.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class GenerateCiJwtSigningKey < ActiveRecord::Migration[6.0] + DOWNTIME = false + + class ApplicationSetting < ActiveRecord::Base + self.table_name = 'application_settings' + + attr_encrypted :ci_jwt_signing_key, { + mode: :per_attribute_iv, + key: Rails.application.secrets.db_key_base[0..31], + algorithm: 'aes-256-gcm', + encode: true + } + end + + def up + ApplicationSetting.reset_column_information + + ApplicationSetting.find_each do |application_setting| + application_setting.update(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem) + end + end + + def down + ApplicationSetting.reset_column_information + + ApplicationSetting.find_each do |application_setting| + application_setting.update_columns(encrypted_ci_jwt_signing_key: nil, encrypted_ci_jwt_signing_key_iv: nil) + end + end +end diff --git a/db/migrate/20201008224441_add_position_into_security_findings.rb b/db/migrate/20201008224441_add_position_into_security_findings.rb new file mode 100644 index 00000000000..ee6d5370f83 --- /dev/null +++ b/db/migrate/20201008224441_add_position_into_security_findings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddPositionIntoSecurityFindings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :security_findings, :position, :integer + end + end + + def down + with_lock_retries do + remove_column :security_findings, :position + end + end +end diff --git a/db/migrate/20201019091307_add_unique_index_on_scan_id_and_position_of_security_findings.rb b/db/migrate/20201019091307_add_unique_index_on_scan_id_and_position_of_security_findings.rb new file mode 100644 index 00000000000..3303e41d8a1 --- /dev/null +++ b/db/migrate/20201019091307_add_unique_index_on_scan_id_and_position_of_security_findings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddUniqueIndexOnScanIdAndPositionOfSecurityFindings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_security_findings_on_scan_id_and_position' + + disable_ddl_transaction! + + def up + add_concurrent_index :security_findings, [:scan_id, :position], unique: true, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :security_findings, INDEX_NAME + end +end diff --git a/db/schema_migrations/20201008011523 b/db/schema_migrations/20201008011523 new file mode 100644 index 00000000000..845ee2f5b96 --- /dev/null +++ b/db/schema_migrations/20201008011523 @@ -0,0 +1 @@ +b7b49ca4c021b7caa9f8612ad9b69d4ec6d79894db2e43266bfe26f2e0bffe08 \ No newline at end of file diff --git a/db/schema_migrations/20201008011937 b/db/schema_migrations/20201008011937 new file mode 100644 index 00000000000..258daf9a836 --- /dev/null +++ b/db/schema_migrations/20201008011937 @@ -0,0 +1 @@ +8af89bb3e63bfca24cee8fdf6f0dd587fae7d81bfeaf6d427f84c7b37c9664ba \ No newline at end of file diff --git a/db/schema_migrations/20201008013434 b/db/schema_migrations/20201008013434 new file mode 100644 index 00000000000..9ec9dd8277c --- /dev/null +++ b/db/schema_migrations/20201008013434 @@ -0,0 +1 @@ +966f6e95189b551cba0ef548cb410911c0beee30d0a265ae21d90321ecbb2a00 \ No newline at end of file diff --git a/db/schema_migrations/20201008224441 b/db/schema_migrations/20201008224441 new file mode 100644 index 00000000000..1f27662d754 --- /dev/null +++ b/db/schema_migrations/20201008224441 @@ -0,0 +1 @@ +d0ca8f0dbe0cf0fbbdd715867f3ae20862683433d919ee5cd942086d21f3b44d \ No newline at end of file diff --git a/db/schema_migrations/20201019091307 b/db/schema_migrations/20201019091307 new file mode 100644 index 00000000000..6b74c804ed8 --- /dev/null +++ b/db/schema_migrations/20201019091307 @@ -0,0 +1 @@ +f19ab0de07415e728849ef4e56804909a3a4a57ad8f55fe71a27bc43c535ac66 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 8f4e5b311d0..3a796e31ef1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9294,9 +9294,12 @@ CREATE TABLE application_settings ( require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL, help_page_documentation_base_url text, automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL, + encrypted_ci_jwt_signing_key text, + encrypted_ci_jwt_signing_key_iv text, CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)), + CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)), CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)), CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)), CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)), @@ -15857,6 +15860,7 @@ CREATE TABLE security_findings ( confidence smallint NOT NULL, project_fingerprint text NOT NULL, deduplicated boolean DEFAULT false NOT NULL, + "position" integer, CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) ); @@ -21528,6 +21532,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated); +CREATE UNIQUE INDEX index_security_findings_on_scan_id_and_position ON security_findings USING btree (scan_id, "position"); + CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id); CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ad3afd6840b..7e35decc18a 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6807,6 +6807,16 @@ type EpicIssue implements CurrentUserTodos & Noteable { """ healthStatus: HealthStatus + """ + Human-readable time estimate of the issue + """ + humanTimeEstimate: String + + """ + Human-readable total time reported as spent on the issue + """ + humanTotalTimeSpent: String + """ Global ID of the epic-issue relation """ @@ -9020,6 +9030,16 @@ type Issue implements CurrentUserTodos & Noteable { """ healthStatus: HealthStatus + """ + Human-readable time estimate of the issue + """ + humanTimeEstimate: String + + """ + Human-readable total time reported as spent on the issue + """ + humanTotalTimeSpent: String + """ ID of the issue """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f093d7e2dd3..2d78538d05d 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -18716,6 +18716,34 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "humanTimeEstimate", + "description": "Human-readable time estimate of the issue", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "humanTotalTimeSpent", + "description": "Human-readable total time reported as spent on the issue", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "id", "description": "Global ID of the epic-issue relation", @@ -24526,6 +24554,34 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "humanTimeEstimate", + "description": "Human-readable time estimate of the issue", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "humanTotalTimeSpent", + "description": "Human-readable total time reported as spent on the issue", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "id", "description": "ID of the issue", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index eeb72239172..ab9ea8d798f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1081,6 +1081,8 @@ Relationship between an epic and an issue. | `epic` | Epic | Epic to which this issue belongs | | `epicIssueId` | ID! | ID of the epic-issue relation | | `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. | +| `humanTimeEstimate` | String | Human-readable time estimate of the issue | +| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue | | `id` | ID | Global ID of the epic-issue relation | | `iid` | ID! | Internal ID of the issue | | `iteration` | Iteration | Iteration of the issue | @@ -1273,6 +1275,8 @@ Represents a recorded measurement (object count) for the Admins. | `dueDate` | Time | Due date of the issue | | `epic` | Epic | Epic to which this issue belongs | | `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. | +| `humanTimeEstimate` | String | Human-readable time estimate of the issue | +| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue | | `id` | ID! | ID of the issue | | `iid` | ID! | Internal ID of the issue | | `iteration` | Iteration | Iteration of the issue | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 2c8f0bbfd33..26ac8159a09 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3574,9 +3574,6 @@ There can be from 2 to 50 jobs. [In GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/26362) and later, you can have one-dimensional matrices with a single job. -The ability to have one-dimensional matrices is [deployed behind a feature flag](../../user/feature_flags.md), -enabled by default. It's enabled on GitLab.com. For self-managed GitLab instances, -administrators can opt to disable it by [disabling the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE ONLY)** Every job gets the same `CI_NODE_TOTAL` [environment variable](../variables/README.md#predefined-environment-variables) value, and a unique `CI_NODE_INDEX` value. diff --git a/doc/user/project/merge_requests/load_performance_testing.md b/doc/user/project/merge_requests/load_performance_testing.md index 2675f509eed..3ee88275aac 100644 --- a/doc/user/project/merge_requests/load_performance_testing.md +++ b/doc/user/project/merge_requests/load_performance_testing.md @@ -156,7 +156,7 @@ The best approach is to capture the dynamic URL in a [`.env` file](https://docs. as a job artifact to be shared, then use a custom environment variable we've provided named `K6_DOCKER_OPTIONS` to configure the k6 Docker container to use the file. With this, k6 can then use any environment variables from the `.env` file in scripts using standard JavaScript, -such as: ``http.get(`${__ENV.ENVIRONMENT_URL`})``. +such as: ``http.get(`${__ENV.ENVIRONMENT_URL}`)``. For example: diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 3a2eeaca3ac..f4a306f825e 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -211,6 +211,7 @@ You can also type in this search bar to see autocomplete suggestions for: - Recently viewed issues (try and type some word from the title of a recently viewed issue) - Recently viewed merge requests (try and type some word from the title of a recently viewed merge request) - Recently viewed epics (try and type some word from the title of a recently viewed epic) +- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues within a project (try and type a GFM reference for an issue) ## Basic search diff --git a/lib/gitlab/ci/config/entry/product/variables.rb b/lib/gitlab/ci/config/entry/product/variables.rb index 2481989060e..aa34cfb3acc 100644 --- a/lib/gitlab/ci/config/entry/product/variables.rb +++ b/lib/gitlab/ci/config/entry/product/variables.rb @@ -14,7 +14,7 @@ module Gitlab validations do validates :config, variables: { array_values: true } validates :config, length: { - minimum: :minimum, + minimum: 1, too_short: 'requires at least %{count} items' } end @@ -28,10 +28,6 @@ module Gitlab .map { |key, value| [key.to_s, Array(value).map(&:to_s)] } .to_h end - - def minimum - ::Gitlab::Ci::Features.one_dimensional_matrix_enabled? ? 1 : 2 - end end end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 1b58e3ec71a..1734f3c9c2b 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -59,10 +59,6 @@ module Gitlab ::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false) end - def self.one_dimensional_matrix_enabled? - ::Feature.enabled?(:one_dimensional_matrix, default_enabled: true) - end - def self.manual_bridges_enabled?(project) ::Feature.enabled?(:ci_manual_bridges, project, default_enabled: true) end diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb index 491facd0a43..2f727a80077 100644 --- a/lib/gitlab/ci/jwt.rb +++ b/lib/gitlab/ci/jwt.rb @@ -6,6 +6,8 @@ module Gitlab NOT_BEFORE_TIME = 5 DEFAULT_EXPIRE_TIME = 60 * 5 + NoSigningKeyError = Class.new(StandardError) + def self.for_build(build) self.new(build, ttl: build.metadata_timeout).encoded end @@ -27,7 +29,7 @@ module Gitlab private - attr_reader :build, :ttl, :key_data + attr_reader :build, :ttl def reserved_claims now = Time.now.to_i @@ -60,7 +62,17 @@ module Gitlab end def key - @key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) + @key ||= begin + key_data = if Feature.enabled?(:ci_jwt_signing_key, build.project) + Gitlab::CurrentSettings.ci_jwt_signing_key + else + Rails.application.secrets.openid_connect_signing_key + end + + raise NoSigningKeyError unless key_data + + OpenSSL::PKey::RSA.new(key_data) + end end def public_key diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index eb132ef0967..2349d5baa10 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -44,7 +44,7 @@ module Gitlab end def unique_events(event_names:, start_date:, end_date:) - events = events_for(Array(event_names)) + events = events_for(Array(event_names).map(&:to_s)) raise 'Events should be in same slot' unless events_in_same_slot?(events) raise 'Events should be in same category' unless events_in_same_category?(events) @@ -141,7 +141,7 @@ module Gitlab end def event_for(event_name) - known_events.find { |event| event[:name] == event_name } + known_events.find { |event| event[:name] == event_name.to_s } end def events_for(event_names) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 352ccdf4d1e..99c51cd2c5e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25558,6 +25558,9 @@ msgstr "" msgid "Successfully scheduled a pipeline to run. Go to the %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details." msgstr "" +msgid "Successfully synced %{synced_timeago}." +msgstr "" + msgid "Successfully unblocked" msgstr "" @@ -25693,12 +25696,18 @@ msgstr "" msgid "Sync information" msgstr "" +msgid "Sync now" +msgstr "" + msgid "Synced" msgstr "" msgid "Synchronization disabled" msgstr "" +msgid "Syncing…" +msgstr "" + msgid "System" msgstr "" diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 164f25389c0..5dd5abc53d7 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -23,7 +23,6 @@ module QA element :merge_button element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern element :merge_moment_dropdown - element :merge_when_pipeline_succeeds_option element :merge_immediately_option end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index d92dae6fc5a..55833ee3aad 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -319,10 +319,10 @@ RSpec.describe GroupsController, factory_default: :keep do stub_experiment(onboarding_issues: false) end - it 'does not track anything' do - expect(Gitlab::Tracking).not_to receive(:event) - + it 'does not track anything', :snowplow do create_namespace + + expect_no_snowplow_event end end @@ -336,15 +336,15 @@ RSpec.describe GroupsController, factory_default: :keep do stub_experiment_for_user(onboarding_issues: false) end - it 'tracks the event with the "created_namespace" action with the "control_group" property' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Conversion::Experiment::OnboardingIssues', - 'created_namespace', + it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do + create_namespace + + expect_snowplow_event( + category: 'Growth::Conversion::Experiment::OnboardingIssues', + action: 'created_namespace', label: anything, property: 'control_group' ) - - create_namespace end end @@ -353,15 +353,15 @@ RSpec.describe GroupsController, factory_default: :keep do stub_experiment_for_user(onboarding_issues: true) end - it 'tracks the event with the "created_namespace" action with the "experimental_group" property' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Conversion::Experiment::OnboardingIssues', - 'created_namespace', + it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do + create_namespace + + expect_snowplow_event( + category: 'Growth::Conversion::Experiment::OnboardingIssues', + action: 'created_namespace', label: anything, property: 'experimental_group' ) - - create_namespace end end end diff --git a/spec/controllers/jwks_controller_spec.rb b/spec/controllers/jwks_controller_spec.rb new file mode 100644 index 00000000000..013ec01eba2 --- /dev/null +++ b/spec/controllers/jwks_controller_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JwksController do + describe 'GET #index' do + let(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(1024) } + let(:ci_jwk) { ci_jwt_signing_key.to_jwk } + let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk } + + before do + stub_application_setting(ci_jwt_signing_key: ci_jwt_signing_key.to_s) + end + + it 'returns signing keys used to sign CI_JOB_JWT' do + get :index + + expect(response).to have_gitlab_http_status(:ok) + + ids = json_response['keys'].map { |jwk| jwk['kid'] } + expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid']) + end + + it 'does not leak private key data' do + get :index + + aggregate_failures do + json_response['keys'].each do |jwk| + expect(jwk.keys).to contain_exactly('kty', 'kid', 'e', 'n', 'use', 'alg') + expect(jwk['use']).to eq('sig') + expect(jwk['alg']).to eq('RS256') + end + end + end + end +end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb index 84f7ae12728..f17720466c0 100644 --- a/spec/db/production/settings_spec.rb +++ b/spec/db/production/settings_spec.rb @@ -62,4 +62,11 @@ RSpec.describe 'seed production settings' do end end end + + context 'CI JWT signing key' do + it 'writes valid RSA key to the database' do + expect { load(settings_file) }.to change { settings.reload.ci_jwt_signing_key }.from(nil) + expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error + end + end end diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index 0fb081ec507..64a357de1f7 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do find('.dropdown-toggle').click Sidekiq::Testing.fake! do - click_link 'Merge immediately' + click_button 'Merge immediately' expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress') diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index 444d5371e7a..5e99383e4a1 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -93,19 +93,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do it_behaves_like 'Merge when pipeline succeeds activator' end end - - describe 'enabling Merge when pipeline succeeds via dropdown' do - it 'activates the Merge when pipeline succeeds feature' do - wait_for_requests - - find('.js-merge-moment').click - click_link 'Merge when pipeline succeeds' - - expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" - expect(page).to have_content "The source branch will not be deleted" - expect(page).to have_link "Cancel automatic merge" - end - end end context 'when merge when pipeline succeeds is enabled' do diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb index 540d87eb969..1d9c80238f5 100644 --- a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb +++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb @@ -44,4 +44,14 @@ RSpec.describe 'Merge Requests > User filters by target branch', :js do expect(page).not_to have_content mr2.title end end + + context 'filtering by target-branch:!=master' do + it 'applies the filter' do + input_filtered_search('target-branch:!=master') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).not_to have_content mr1.title + expect(page).to have_content mr2.title + end + end end diff --git a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js index 191f91be076..3f99768aa08 100644 --- a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js +++ b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js @@ -1,6 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { GlFormInput, GlFormTextarea } from '@gitlab/ui'; import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue'; @@ -8,8 +7,6 @@ import EditMetaControls from '~/static_site_editor/components/edit_meta_controls import { mergeRequestMeta } from '../mock_data'; describe('~/static_site_editor/components/edit_meta_controls.vue', () => { - useLocalStorageSpy(); - let wrapper; let mockSelect; let mockGlFormInputTitleInstance; @@ -86,14 +83,5 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => { expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings); }); - - it('should remember the input changes', () => { - findGlFormInputTitle().vm.$emit('input', newTitle); - findGlFormTextAreaDescription().vm.$emit('input', newDescription); - - const newSettings = { title: newTitle, description: newDescription }; - - expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, JSON.stringify(newSettings)); - }); }); }); diff --git a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js index 7a5685033f3..da4c225d25b 100644 --- a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js +++ b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js @@ -1,13 +1,15 @@ import { shallowMount } from '@vue/test-utils'; - import { GlModal } from '@gitlab/ui'; - +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue'; import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue'; - +import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants'; import { sourcePath, mergeRequestMeta } from '../mock_data'; describe('~/static_site_editor/components/edit_meta_modal.vue', () => { + useLocalStorageSpy(); + let wrapper; let resetCachedEditable; let mockEditMetaControlsInstance; @@ -30,6 +32,11 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { const findGlModal = () => wrapper.find(GlModal); const findEditMetaControls = () => wrapper.find(EditMetaControls); + const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + + beforeEach(() => { + localStorage.setItem(MR_META_LOCAL_STORAGE_KEY); + }); beforeEach(() => { buildWrapper(); @@ -43,6 +50,16 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { wrapper = null; }); + it('initializes initial merge request meta with local storage data', async () => { + const localStorageMeta = { title: 'stored title', description: 'stored description' }; + + findLocalStorageSync().vm.$emit('input', localStorageMeta); + + await wrapper.vm.$nextTick(); + + expect(findEditMetaControls().props()).toEqual(localStorageMeta); + }); + it('renders the modal', () => { expect(findGlModal().exists()).toBe(true); }); @@ -63,18 +80,32 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { expect(findEditMetaControls().props('description')).toBe(description); }); - it('emits the primary event with mergeRequestMeta', () => { - findGlModal().vm.$emit('primary', mergeRequestMeta); - expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]); - }); + describe('when save button is clicked', () => { + beforeEach(() => { + findGlModal().vm.$emit('primary', mergeRequestMeta); + }); - it('calls resetCachedEditable on EditMetaControls when primary emits', () => { - findGlModal().vm.$emit('primary', mergeRequestMeta); - expect(mockEditMetaControlsInstance.resetCachedEditable).toHaveBeenCalled(); + it('removes merge request meta from local storage', () => { + expect(findLocalStorageSync().props().clear).toBe(true); + }); + + it('emits the primary event with mergeRequestMeta', () => { + expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]); + }); }); it('emits the hide event', () => { findGlModal().vm.$emit('hide'); expect(wrapper.emitted('hide')).toEqual([[]]); }); + + it('stores merge request meta changes in local storage when changes happen', async () => { + const newMeta = { title: 'new title', description: 'new description' }; + + findEditMetaControls().vm.$emit('updateSettings', newMeta); + + await wrapper.vm.$nextTick(); + + expect(findLocalStorageSync().props('value')).toEqual(newMeta); + }); }); diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js index efa9b5796fb..464fe3411dd 100644 --- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -239,4 +239,30 @@ describe('Local Storage Sync', () => { }); }); }); + + it('clears localStorage when clear property is true', async () => { + const storageKey = 'key'; + const value = 'initial'; + + createComponent({ + props: { + storageKey, + }, + }); + wrapper.setProps({ + value, + }); + + await wrapper.vm.$nextTick(); + + expect(localStorage.getItem(storageKey)).toBe(value); + + wrapper.setProps({ + clear: true, + }); + + await wrapper.vm.$nextTick(); + + expect(localStorage.getItem(storageKey)).toBe(null); + }); }); diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index c55e624dd11..be45dbdc2e3 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -16,7 +16,7 @@ RSpec.describe GitlabSchema.types['Issue'] do it 'has specific fields' do fields = %i[id iid title description state reference author assignees participants labels milestone due_date confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position - subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status + subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status designs design_collection alert_management_alert severity current_user_todos] fields.each do |field_name| diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 6fe071521cd..00f118e1829 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -73,7 +73,7 @@ RSpec.describe SearchHelper do expect(result.keys).to match_array(%i[category id label url avatar_url]) end - it 'includes the users recently viewed issues' do + it 'includes the users recently viewed issues', :aggregate_failures do recent_issues = instance_double(::Gitlab::Search::RecentIssues) expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues) project1 = create(:project, :with_avatar, namespace: user.namespace) @@ -104,7 +104,7 @@ RSpec.describe SearchHelper do }) end - it 'includes the users recently viewed merge requests' do + it 'includes the users recently viewed merge requests', :aggregate_failures do recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests) expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests) project1 = create(:project, :with_avatar, namespace: user.namespace) @@ -145,10 +145,40 @@ RSpec.describe SearchHelper do @project = create(:project, :repository) end - it "includes project-specific sections" do + it "includes project-specific sections", :aggregate_failures do expect(search_autocomplete_opts("Files").size).to eq(1) expect(search_autocomplete_opts("Commits").size).to eq(1) end + + context 'when user does not have access to project' do + it 'does not include issues by iid' do + issue = create(:issue, project: @project) + results = search_autocomplete_opts("\##{issue.iid}") + + expect(results.count).to eq(0) + end + end + + context 'when user has project access' do + before do + @project = create(:project, :repository, namespace: user.namespace) + end + + it 'includes issues by iid', :aggregate_failures do + issue = create(:issue, project: @project, title: 'test title') + results = search_autocomplete_opts("\##{issue.iid}") + + expect(results.count).to eq(1) + + expect(results.first).to include({ + category: 'In this project', + id: issue.id, + label: 'test title (#1)', + url: ::Gitlab::Routing.url_helpers.project_issue_path(issue.project, issue), + avatar_url: '' # project has no avatar + }) + end + end end end diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb index 3388ae0af2f..ff44a235ea5 100644 --- a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb @@ -46,17 +46,41 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do end end - context 'with one_dimensional_matrix feature flag enabled' do - before do - stub_feature_flags(one_dimensional_matrix: true) - matrix.compose! + context 'when entry config has only one variable with multiple values' do + let(:config) do + [ + { + 'VAR_1' => %w[build test] + } + ] end - context 'when entry config has only one variable with multiple values' do + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#errors' do + it 'returns no errors' do + expect(matrix.errors) + .to be_empty + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }]) + end + end + + context 'when entry config has only one variable with one value' do let(:config) do [ { - 'VAR_1' => %w[build test] + 'VAR_1' => %w[test] } ] end @@ -78,107 +102,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do end it 'returns the value without raising an error' do - expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }]) - end - end - - context 'when entry config has only one variable with one value' do - let(:config) do - [ - { - 'VAR_1' => %w[test] - } - ] - end - - describe '#valid?' do - it { is_expected.to be_valid } - end - - describe '#errors' do - it 'returns no errors' do - expect(matrix.errors) - .to be_empty - end - end - - describe '#value' do - before do - matrix.compose! - end - - it 'returns the value without raising an error' do - expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }]) - end - end - end - end - end - - context 'with one_dimensional_matrix feature flag disabled' do - before do - stub_feature_flags(one_dimensional_matrix: false) - matrix.compose! - end - - context 'when entry config has only one variable with multiple values' do - let(:config) do - [ - { - 'VAR_1' => %w[build test] - } - ] - end - - describe '#valid?' do - it { is_expected.not_to be_valid } - end - - describe '#errors' do - it 'returns error about too many jobs' do - expect(matrix.errors) - .to include('variables config requires at least 2 items') - end - end - - describe '#value' do - before do - matrix.compose! - end - - it 'returns the value without raising an error' do - expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }]) - end - end - - context 'when entry config has only one variable with one value' do - let(:config) do - [ - { - 'VAR_1' => %w[test] - } - ] - end - - describe '#valid?' do - it { is_expected.not_to be_valid } - end - - describe '#errors' do - it 'returns no errors' do - expect(matrix.errors) - .to include('variables config requires at least 2 items') - end - end - - describe '#value' do - before do - matrix.compose! - end - - it 'returns the value without raising an error' do - expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }]) - end + expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }]) end end end diff --git a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb index 407efb438b5..5e920ce34e0 100644 --- a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -# After Feature one_dimensional_matrix is removed, this can be changed back to fast_spec_helper -require 'spec_helper' +require 'fast_spec_helper' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do @@ -46,70 +45,18 @@ RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do end end - context 'with one_dimensional_matrix feature flag enabled' do - context 'with only one variable' do - before do - stub_feature_flags(one_dimensional_matrix: true) - end - let(:config) { { VAR: 'test' } } + context 'with only one variable' do + let(:config) { { VAR: 'test' } } - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end - end - - describe '#errors' do - it 'does not append errors' do - expect(entry.errors).to be_empty - end + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid end end - end - context 'with one_dimensional_matrix feature flag disabled' do - context 'when entry value is not correct' do - before do - stub_feature_flags(one_dimensional_matrix: false) - end - shared_examples 'invalid variables' do |message| - describe '#errors' do - it 'saves errors' do - expect(entry.errors).to include(message) - end - end - - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - end - - context 'with array' do - let(:config) { [:VAR, 'test'] } - - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end - - context 'with empty array' do - let(:config) { { VAR: 'test', VAR2: [] } } - - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end - - context 'with nested array' do - let(:config) { { VAR: 'test', VAR2: [1, [2]] } } - - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end - - context 'with one_dimensional_matrix feature flag disabled' do - context 'with only one variable' do - let(:config) { { VAR: 'test' } } - - it_behaves_like 'invalid variables', /variables config requires at least 2 items/ - end + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty end end end diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 9b133efad9c..3130c0c0c41 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -93,32 +93,65 @@ RSpec.describe Gitlab::Ci::Jwt do end describe '.for_build' do - let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) } + shared_examples 'generating JWT for build' do + context 'when signing key is present' do + let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) } + let(:rsa_key_data) { rsa_key.to_s } + + it 'generates JWT with key id' do + _payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) + + expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid']) + end + + it 'generates JWT for the given job with ttl equal to build timeout' do + expect(build).to receive(:metadata_timeout).and_return(3_600) + + payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) + ttl = payload["exp"] - payload["iat"] + + expect(ttl).to eq(3_600) + end + + it 'generates JWT for the given job with default ttl if build timeout is not set' do + expect(build).to receive(:metadata_timeout).and_return(nil) + + payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) + ttl = payload["exp"] - payload["iat"] + + expect(ttl).to eq(5.minutes.to_i) + end + end + + context 'when signing key is missing' do + let(:rsa_key_data) { nil } + + it 'raises NoSigningKeyError' do + expect { jwt }.to raise_error described_class::NoSigningKeyError + end + end + end subject(:jwt) { described_class.for_build(build) } - it 'generates JWT with key id' do - _payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) + context 'when ci_jwt_signing_key feature flag is disabled' do + before do + stub_feature_flags(ci_jwt_signing_key: false) - expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid']) + allow(Rails.application.secrets).to receive(:openid_connect_signing_key).and_return(rsa_key_data) + end + + it_behaves_like 'generating JWT for build' end - it 'generates JWT for the given job with ttl equal to build timeout' do - expect(build).to receive(:metadata_timeout).and_return(3_600) + context 'when ci_jwt_signing_key feature flag is enabled' do + before do + stub_feature_flags(ci_jwt_signing_key: true) - payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) - ttl = payload["exp"] - payload["iat"] + stub_application_setting(ci_jwt_signing_key: rsa_key_data) + end - expect(ttl).to eq(3_600) - end - - it 'generates JWT for the given job with default ttl if build timeout is not set' do - expect(build).to receive(:metadata_timeout).and_return(nil) - - payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) - ttl = payload["exp"] - payload["iat"] - - expect(ttl).to eq(5.minutes.to_i) + it_behaves_like 'generating JWT for build' end end end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index e84c3c17274..10a69c8ea07 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -77,6 +77,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s stub_application_setting(usage_ping_enabled: true) end + it 'tracks event when using symbol' do + expect(Gitlab::Redis::HLL).to receive(:add) + + described_class.track_event(entity1, :g_analytics_contribution) + end + it "raise error if metrics don't have same aggregation" do expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation) end @@ -201,6 +207,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it { expect(described_class.unique_events(event_names: weekly_event, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) } end + context 'when using symbol as parameter' do + it { expect(described_class.unique_events(event_names: weekly_event.to_sym, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) } + end + context 'when using daily aggregation' do it { expect(described_class.unique_events(event_names: daily_event, start_date: 7.days.ago, end_date: Date.current)).to eq(2) } it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: Date.current)).to eq(3) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index fa91bbc36b2..3cc5f202b1f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -619,6 +619,7 @@ RSpec.describe Notify do let(:mailer) do mailer = described_class.new mailer.instance_variable_set(:@note, mail_thread_note) + mailer.instance_variable_set(:@target_url, "https://some.link") mailer end diff --git a/spec/migrations/generate_ci_jwt_signing_key_spec.rb b/spec/migrations/generate_ci_jwt_signing_key_spec.rb new file mode 100644 index 00000000000..4cfaa8701aa --- /dev/null +++ b/spec/migrations/generate_ci_jwt_signing_key_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'migrate', '20201008013434_generate_ci_jwt_signing_key.rb') + +RSpec.describe GenerateCiJwtSigningKey do + let(:application_settings) do + Class.new(ActiveRecord::Base) do + self.table_name = 'application_settings' + + attr_encrypted :ci_jwt_signing_key, { + mode: :per_attribute_iv, + key: Rails.application.secrets.db_key_base[0..31], + algorithm: 'aes-256-gcm', + encode: true + } + end + end + + it 'generates JWT signing key' do + application_settings.create! + + reversible_migration do |migration| + migration.before -> { + settings = application_settings.first + + expect(settings.ci_jwt_signing_key).to be_nil + expect(settings.encrypted_ci_jwt_signing_key).to be_nil + expect(settings.encrypted_ci_jwt_signing_key_iv).to be_nil + } + + migration.after -> { + settings = application_settings.first + + expect(settings.encrypted_ci_jwt_signing_key).to be_present + expect(settings.encrypted_ci_jwt_signing_key_iv).to be_present + expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error + } + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fb702d10a42..26c97462bf2 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -647,6 +647,23 @@ RSpec.describe ApplicationSetting do end end end + + describe '#ci_jwt_signing_key' do + it { is_expected.not_to allow_value('').for(:ci_jwt_signing_key) } + it { is_expected.not_to allow_value('invalid RSA key').for(:ci_jwt_signing_key) } + it { is_expected.to allow_value(nil).for(:ci_jwt_signing_key) } + it { is_expected.to allow_value(OpenSSL::PKey::RSA.new(1024).to_pem).for(:ci_jwt_signing_key) } + + it 'is encrypted' do + subject.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(1024).to_pem + + aggregate_failures do + expect(subject.encrypted_ci_jwt_signing_key).to be_present + expect(subject.encrypted_ci_jwt_signing_key_iv).to be_present + expect(subject.encrypted_ci_jwt_signing_key).not_to eq(subject.ci_jwt_signing_key) + end + end + end end context 'static objects external storage' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f1d51324bbf..5ff9b4dd493 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do end before do - allow(Gitlab::Ci::Jwt).to receive(:for_build).with(build).and_return('ci.job.jwt') + allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt') build.set_token('my-token') build.yaml_variables = [] end @@ -2482,12 +2482,17 @@ RSpec.describe Ci::Build do end context 'when CI_JOB_JWT generation fails' do - it 'CI_JOB_JWT is not included' do - expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(OpenSSL::PKey::RSAError, 'Neither PUB key nor PRIV key: not enough data') - expect(Gitlab::ErrorTracking).to receive(:track_exception) + [ + OpenSSL::PKey::RSAError, + Gitlab::Ci::Jwt::NoSigningKeyError + ].each do |reason_to_fail| + it 'CI_JOB_JWT is not included' do + expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(reason_to_fail) + expect(Gitlab::ErrorTracking).to receive(:track_exception) - expect { subject }.not_to raise_error - expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') + expect { subject }.not_to raise_error + expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 877188097fd..9824eb91bc7 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -493,104 +493,49 @@ RSpec.describe CommitStatus do end end - context 'with the one_dimensional_matrix feature flag disabled' do - describe '#group_name' do - before do - stub_feature_flags(one_dimensional_matrix: false) - end + describe '#group_name' do + using RSpec::Parameterized::TableSyntax - let(:commit_status) do - build(:commit_status, pipeline: pipeline, stage: 'test') - end - - subject { commit_status.group_name } - - tests = { - 'rspec:windows' => 'rspec:windows', - 'rspec:windows 0' => 'rspec:windows 0', - 'rspec:windows 0 test' => 'rspec:windows 0 test', - 'rspec:windows 0 1' => 'rspec:windows', - 'rspec:windows 0 1 name' => 'rspec:windows name', - 'rspec:windows 0/1' => 'rspec:windows', - 'rspec:windows 0/1 name' => 'rspec:windows name', - 'rspec:windows 0:1' => 'rspec:windows', - 'rspec:windows 0:1 name' => 'rspec:windows name', - 'rspec:windows 10000 20000' => 'rspec:windows', - 'rspec:windows 0 : / 1' => 'rspec:windows', - 'rspec:windows 0 : / 1 name' => 'rspec:windows name', - '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby', - 'rspec: [aws]' => 'rspec: [aws]', - 'rspec: [aws] 0/1' => 'rspec: [aws]', - 'rspec: [aws, max memory]' => 'rspec', - 'rspec:linux: [aws, max memory, data]' => 'rspec:linux', - 'rspec: [inception: [something, other thing], value]' => 'rspec', - 'rspec:windows 0/1: [name, other]' => 'rspec:windows', - 'rspec:windows: [name, other] 0/1' => 'rspec:windows', - 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows', - 'rspec:windows: [0/1, name]' => 'rspec:windows', - 'rspec:windows: [, ]' => 'rspec:windows', - 'rspec:windows: [name]' => 'rspec:windows: [name]', - 'rspec:windows: [name,other]' => 'rspec:windows: [name,other]' - } - - tests.each do |name, group_name| - it "'#{name}' puts in '#{group_name}'" do - commit_status.name = name - - is_expected.to eq(group_name) - end - end + let(:commit_status) do + build(:commit_status, pipeline: pipeline, stage: 'test') end - end - context 'with one_dimensional_matrix feature flag enabled' do - describe '#group_name' do - before do - stub_feature_flags(one_dimensional_matrix: true) - end + subject { commit_status.group_name } - let(:commit_status) do - build(:commit_status, pipeline: pipeline, stage: 'test') - end + where(:name, :group_name) do + 'rspec:windows' | 'rspec:windows' + 'rspec:windows 0' | 'rspec:windows 0' + 'rspec:windows 0 test' | 'rspec:windows 0 test' + 'rspec:windows 0 1' | 'rspec:windows' + 'rspec:windows 0 1 name' | 'rspec:windows name' + 'rspec:windows 0/1' | 'rspec:windows' + 'rspec:windows 0/1 name' | 'rspec:windows name' + 'rspec:windows 0:1' | 'rspec:windows' + 'rspec:windows 0:1 name' | 'rspec:windows name' + 'rspec:windows 10000 20000' | 'rspec:windows' + 'rspec:windows 0 : / 1' | 'rspec:windows' + 'rspec:windows 0 : / 1 name' | 'rspec:windows name' + '0 1 name ruby' | 'name ruby' + '0 :/ 1 name ruby' | 'name ruby' + 'rspec: [aws]' | 'rspec' + 'rspec: [aws] 0/1' | 'rspec' + 'rspec: [aws, max memory]' | 'rspec' + 'rspec:linux: [aws, max memory, data]' | 'rspec:linux' + 'rspec: [inception: [something, other thing], value]' | 'rspec' + 'rspec:windows 0/1: [name, other]' | 'rspec:windows' + 'rspec:windows: [name, other] 0/1' | 'rspec:windows' + 'rspec:windows: [name, 0/1] 0/1' | 'rspec:windows' + 'rspec:windows: [0/1, name]' | 'rspec:windows' + 'rspec:windows: [, ]' | 'rspec:windows' + 'rspec:windows: [name]' | 'rspec:windows' + 'rspec:windows: [name,other]' | 'rspec:windows' + end - subject { commit_status.group_name } + with_them do + it "#{params[:name]} puts in #{params[:group_name]}" do + commit_status.name = name - tests = { - 'rspec:windows' => 'rspec:windows', - 'rspec:windows 0' => 'rspec:windows 0', - 'rspec:windows 0 test' => 'rspec:windows 0 test', - 'rspec:windows 0 1' => 'rspec:windows', - 'rspec:windows 0 1 name' => 'rspec:windows name', - 'rspec:windows 0/1' => 'rspec:windows', - 'rspec:windows 0/1 name' => 'rspec:windows name', - 'rspec:windows 0:1' => 'rspec:windows', - 'rspec:windows 0:1 name' => 'rspec:windows name', - 'rspec:windows 10000 20000' => 'rspec:windows', - 'rspec:windows 0 : / 1' => 'rspec:windows', - 'rspec:windows 0 : / 1 name' => 'rspec:windows name', - '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby', - 'rspec: [aws]' => 'rspec', - 'rspec: [aws] 0/1' => 'rspec', - 'rspec: [aws, max memory]' => 'rspec', - 'rspec:linux: [aws, max memory, data]' => 'rspec:linux', - 'rspec: [inception: [something, other thing], value]' => 'rspec', - 'rspec:windows 0/1: [name, other]' => 'rspec:windows', - 'rspec:windows: [name, other] 0/1' => 'rspec:windows', - 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows', - 'rspec:windows: [0/1, name]' => 'rspec:windows', - 'rspec:windows: [, ]' => 'rspec:windows', - 'rspec:windows: [name]' => 'rspec:windows', - 'rspec:windows: [name,other]' => 'rspec:windows' - } - - tests.each do |name, group_name| - it "'#{name}' puts in '#{group_name}'" do - commit_status.name = name - - is_expected.to eq(group_name) - end + is_expected.to eq(group_name) end end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 9af620e70a5..2b24e2d6455 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -4,9 +4,10 @@ require 'spec_helper' RSpec.describe GroupMember do context 'scopes' do + let_it_be(:user_1) { create(:user) } + let_it_be(:user_2) { create(:user) } + it 'counts users by group ID' do - user_1 = create(:user) - user_2 = create(:user) group_1 = create(:group) group_2 = create(:group) @@ -25,6 +26,15 @@ RSpec.describe GroupMember do expect(described_class.of_ldap_type).to eq([group_member]) end end + + describe '.with_user' do + it 'returns requested user' do + group_member = create(:group_member, user: user_2) + create(:group_member, user: user_1) + + expect(described_class.with_user(user_2)).to eq([group_member]) + end + end end describe '.access_level_roles' do diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb index b5291953730..dc9190114fd 100644 --- a/spec/routing/openid_connect_spec.rb +++ b/spec/routing/openid_connect_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' # oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys -# jwks GET /-/jwks(.:format) doorkeeper/openid_connect/discovery#keys # oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider # oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do @@ -18,10 +17,6 @@ RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do it "to #keys" do expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') end - - it "/-/jwks" do - expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys') - end end # oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index f665dc31ee4..76ccdf3237c 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -369,3 +369,10 @@ RSpec.describe RunnerSetupController, 'routing' do expect(get("/-/runner_setup/platforms")).to route_to('runner_setup#platforms') end end + +# jwks GET /-/jwks(.:format) jwks#index +RSpec.describe JwksController, "routing" do + it "to #index" do + expect(get('/-/jwks')).to route_to('jwks#index') + end +end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 13f7cd62002..698804ff6af 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -161,10 +161,10 @@ RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execu expect(application.status_reason).to be_nil end - it 'tracks application install' do - expect(Gitlab::Tracking).to receive(:event).with('cluster:applications', "cluster_application_helm_installed") - + it 'tracks application install', :snowplow do service.execute + + expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed') end end diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb index b095cb24212..8e8adc516cf 100644 --- a/spec/services/issues/zoom_link_service_spec.rb +++ b/spec/services/issues/zoom_link_service_spec.rb @@ -46,10 +46,15 @@ RSpec.describe Issues::ZoomLinkService do expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link) end - it 'tracks the add event' do - expect(Gitlab::Tracking).to receive(:event) - .with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id) + it 'tracks the add event', :snowplow do result + + expect_snowplow_event( + category: 'IncidentManagement::ZoomIntegration', + action: 'add_zoom_meeting', + label: 'Issue ID', + value: issue.id + ) end it 'creates a zoom_link_added notification' do @@ -180,10 +185,15 @@ RSpec.describe Issues::ZoomLinkService do expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil) end - it 'tracks the remove event' do - expect(Gitlab::Tracking).to receive(:event) - .with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id) + it 'tracks the remove event', :snowplow do result + + expect_snowplow_event( + category: 'IncidentManagement::ZoomIntegration', + action: 'remove_zoom_meeting', + label: 'Issue ID', + value: issue.id + ) end end diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb index 3bde01c6fbf..0c6d2bfad63 100644 --- a/spec/support/helpers/snowplow_helpers.rb +++ b/spec/support/helpers/snowplow_helpers.rb @@ -36,10 +36,10 @@ module SnowplowHelpers # would not pass any arguments when using **kwargs. # https://gitlab.com/gitlab-org/gitlab/-/issues/263430 if kwargs.present? - expect(Gitlab::Tracking).to have_received(:event) + expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking .with(category, action, **kwargs).at_least(:once) else - expect(Gitlab::Tracking).to have_received(:event) + expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking .with(category, action).at_least(:once) end end @@ -56,6 +56,6 @@ module SnowplowHelpers # end # end def expect_no_snowplow_event - expect(Gitlab::Tracking).not_to have_received(:event) + expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking end end diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb index 58812b8f4e6..ae00e30a191 100644 --- a/spec/support/snowplow.rb +++ b/spec/support/snowplow.rb @@ -13,7 +13,7 @@ RSpec.configure do |config| stub_application_setting(snowplow_enabled: true) - allow(Gitlab::Tracking).to receive(:event).and_call_original + allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking end config.after(:each, :snowplow) do diff --git a/spec/validators/rsa_key_validator_spec.rb b/spec/validators/rsa_key_validator_spec.rb new file mode 100644 index 00000000000..b4e74ec5605 --- /dev/null +++ b/spec/validators/rsa_key_validator_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RsaKeyValidator do + let(:validatable) do + Class.new do + include ActiveModel::Validations + + attr_accessor :signing_key + + validates :signing_key, rsa_key: true + + def initialize(signing_key) + @signing_key = signing_key + end + end + end + + subject(:validator) { described_class.new(attributes: [:signing_key]) } + + it 'is not valid when invalid RSA key is provided' do + record = validatable.new('invalid RSA key') + + validator.validate(record) + + aggregate_failures do + expect(record).not_to be_valid + expect(record.errors[:signing_key]).to include('is not a valid RSA key') + end + end + + it 'is valid when valid RSA key is provided' do + record = validatable.new(OpenSSL::PKey::RSA.new(1024).to_pem) + + validator.validate(record) + + expect(record).to be_valid + end +end