diff options
39 files changed, 665 insertions, 546 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 168f60f0f65..279800cb230 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -15,7 +15,7 @@ code_quality: stage: test needs: [] variables: - CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18" script: - | if ! docker info &>/dev/null; then diff --git a/.gitpod.yml b/.gitpod.yml index d83e5ff3e61..2c6aa2a962d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -14,6 +14,7 @@ tasks: set -e cd /workspace/gitlab-development-kit [[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab + mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config # make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace echo "webpack:" >> gdk.yml echo " static: true" >> gdk.yml @@ -48,14 +49,6 @@ tasks: if [ "$GITLAB_RUN_DB_MIGRATIONS" == true ]; then make gitlab-db-migrate fi - # Fix DB key - if [ "$GITLAB_FIX_DB_KEY" = true ]; then - echo "$(date) – Fixing DB key" | tee -a /workspace/startup.log - cd gitlab - # see https://gitlab.com/gitlab-org/gitlab-foss/-/issues/56403#note_132515069 - printf 'ApplicationSetting.last.update_column(:runners_registration_token_encrypted, nil)\nexit\n' | bundle exec rails c - cd - - fi # Waiting for GitLab ... gp await-port 3000 printf "Waiting for GitLab at $(gp url 3000) ..." diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 7055cd42978..17e6255700a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -5,11 +5,11 @@ import { uniq } from 'lodash'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import Cookies from 'js-cookie'; import { __ } from './locale'; -import { updateTooltipTitle } from './lib/utils/common_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { deprecatedCreateFlash as flash } from './flash'; import axios from './lib/utils/axios_utils'; import * as Emoji from '~/emoji'; +import { dispose, fixTitle } from '~/tooltips'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -374,7 +374,7 @@ export class AwardsHandler { counter.text(counterNumber - 1); this.removeYouFromUserList($emojiButton); } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('dispose'); + dispose($emojiButton); counter.text('0'); this.removeYouFromUserList($emojiButton); if ($emojiButton.parents('.note').length) { @@ -387,7 +387,8 @@ export class AwardsHandler { } removeEmoji($emojiButton) { - $emojiButton.tooltip('dispose'); + dispose($emojiButton); + $emojiButton.remove(); const $votesBlock = this.getVotesBlock(); if ($votesBlock.find('.js-emoji-btn').length === 0) { @@ -415,13 +416,17 @@ export class AwardsHandler { const originalTitle = this.getAwardTooltip(awardBlock); const authors = originalTitle.split(FROM_SENTENCE_REGEX); authors.splice(authors.indexOf('You'), 1); - return awardBlock + + awardBlock .closest('.js-emoji-btn') .removeData('title') .removeAttr('data-title') .removeAttr('data-original-title') - .attr('title', this.toSentence(authors)) - .tooltip('_fixTitle'); + .attr('title', this.toSentence(authors)); + + fixTitle(awardBlock); + + return awardBlock; } addYouToUserList(votesBlock, emoji) { @@ -432,7 +437,12 @@ export class AwardsHandler { users = origTitle.trim().split(FROM_SENTENCE_REGEX); } users.unshift('You'); - return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle'); + + awardBlock.attr('title', this.toSentence(users)); + + fixTitle(awardBlock); + + return awardBlock; } createAwardButtonForVotesBlock(votesBlock, emojiName) { @@ -448,7 +458,7 @@ export class AwardsHandler { .find('.emoji-icon') .data('name', emojiName); this.animateEmoji($emojiButton); - $('.award-control').tooltip(); + votesBlock.removeClass('current'); } @@ -487,17 +497,6 @@ export class AwardsHandler { return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); } - userAuthored($emojiButton) { - const oldTitle = this.getAwardTooltip($emojiButton); - const newTitle = 'You cannot vote on your own issue, MR and note'; - updateTooltipTitle($emojiButton, newTitle).tooltip('show'); - // Restore tooltip back to award list - return setTimeout(() => { - $emojiButton.tooltip('hide'); - updateTooltipTitle($emojiButton, oldTitle); - }, 2800); - } - scrollToAwards() { const options = { scrollTop: $('.awards').offset().top - 110, diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index fe1ac00fd1d..7845f2968db 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -61,9 +61,6 @@ export const rstrip = val => { return val; }; -export const updateTooltipTitle = ($tooltipEl, newTitle) => - $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); - export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { const field = $(fieldSelector); const closestSubmit = field.closest('form').find(buttonSelector); diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 58cda84ebe7..9fb5976e11a 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -6,6 +6,13 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController layout 'fullscreen' + content_security_policy do |policy| + next if policy.directives.blank? + + frame_src_values = Array.wrap(policy.directives['frame-src']) | ['https://www.youtube.com'] + policy.frame_src(*frame_src_values) + end + prepend_before_action :authenticate_user!, only: [:show] before_action :assign_ref_and_path, only: [:show] before_action :authorize_edit_tree!, only: [:show] diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 6a74b7010b9..577d8d5cb2a 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -377,7 +377,12 @@ module IssuablesHelper end def issuable_display_type(issuable) - issuable.model_name.human.downcase + case issuable + when Issue + issuable.issue_type.downcase + when MergeRequest + issuable.model_name.human.downcase + end end def has_filter_bar_param? diff --git a/app/models/snippet.rb b/app/models/snippet.rb index d71853e11cf..dc370b46bda 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -293,9 +293,7 @@ class Snippet < ApplicationRecord @storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX) end - # This is the full_path used to identify the - # the snippet repository. It will be used mostly - # for logging purposes. + # This is the full_path used to identify the the snippet repository. override :full_path def full_path return unless persisted? @@ -303,7 +301,7 @@ class Snippet < ApplicationRecord @full_path ||= begin components = [] components << project.full_path if project_id? - components << '@snippets' + components << 'snippets' components << self.id components.join('/') end diff --git a/app/views/projects/incidents/show.html.haml b/app/views/projects/incidents/show.html.haml index b0ddc85df5d..4d4607e8e36 100644 --- a/app/views/projects/incidents/show.html.haml +++ b/app/views/projects/incidents/show.html.haml @@ -1 +1,6 @@ -= render template: 'projects/issues/show' +- @content_class = "limit-container-width" unless fluid_layout +- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project) +- breadcrumb_title @issue.to_reference +- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents") + += render 'projects/issuable/show', issuable: @issue diff --git a/app/views/projects/issuable/_show.html.haml b/app/views/projects/issuable/_show.html.haml new file mode 100644 index 00000000000..48920c4e342 --- /dev/null +++ b/app/views/projects/issuable/_show.html.haml @@ -0,0 +1,10 @@ +- page_description issuable.description_html +- page_card_attributes issuable.card_attributes +- if issuable.relocation_target + - page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url + += render_if_exists "projects/issues/alert_blocked", issue: issuable, current_user: current_user += render "projects/issues/alert_moved_from_service_desk", issue: issuable + += render 'shared/issue_type/details_header', issuable: issuable += render 'shared/issue_type/details_content', issuable: issuable diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 4778221335e..c3949a83e3f 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,103 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout - add_to_breadcrumbs _("Issues"), project_issues_path(@project) - breadcrumb_title @issue.to_reference -- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") -- page_description @issue.description_html -- page_card_attributes @issue.card_attributes -- if @issue.relocation_target - - page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url -- if @issue.sentry_issue.present? - - add_page_specific_style 'page_bundles/error_tracking_details' +- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") -- can_update_issue = can?(current_user, :update_issue, @issue) -- can_reopen_issue = can?(current_user, :reopen_issue, @issue) -- can_report_spam = @issue.submittable_as_spam_by?(current_user) -- can_create_issue = show_new_issue_link?(@project) -- related_branches_path = related_branches_project_issue_path(@project, @issue) - -= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user -= render "projects/issues/alert_moved_from_service_desk", issue: @issue - -.detail-page-header - .detail-page-header-body - .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) } - = sprite_icon('mobile-issue-close', css_class: 'd-block d-sm-none') - .d-none.d-sm-block - = issue_closed_text(@issue, current_user) - .issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(@issue, status_box: :open) } - = sprite_icon('issue-open-m', css_class: 'd-block d-sm-none') - %span.d-none.d-sm-block Open - - .issuable-meta - #js-issuable-header-warnings - = issuable_meta(@issue, @project, "Issue") - - %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } - = sprite_icon('chevron-double-lg-left') - - .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } - .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.float-left.d-md-none{ type: "button", data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-right.d-lg-none - %ul - - unless current_user == @issue.author - %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - - if can_update_issue - %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' - - if can_reopen_issue - %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - - if can_report_spam - %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' - - if can_create_issue - - if can_update_issue || can_report_spam - %li.divider - %li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link' - - = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked? - - - if can_report_spam - = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' - - if can_create_issue - = link_to new_project_issue_path(@project), class: 'd-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do - New issue - -.issue-details.issuable-details - .detail-page-description.content-block - #js-issuable-app{ data: { initial: issuable_initial_data(@issue).to_json} } - .title-container - %h2.title= markdown_field(@issue, :title) - - if @issue.description.present? - .description - .md= markdown_field(@issue, :description) - - = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') - - - if @issue.sentry_issue.present? - #js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) } - - = render 'projects/issues/design_management' - - = render_if_exists 'projects/issues/related_issues' - - #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } - - - if can?(current_user, :download_code, @project) - - add_page_startup_api_call related_branches_path - #related-branches{ data: { url: related_branches_path } } - -# This element is filled in using JavaScript. - - .content-block.emoji-block.emoji-block-sticky - .row.gl-m-0.gl-justify-content-space-between - .js-noteable-awards - = render 'award_emoji/awards_block', awardable: @issue, inline: true - .new-branch-col - = render_if_exists "projects/issues/timeline_toggle", issue: @issue - #js-vue-sort-issue-discussions - #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } - = render 'new_branch' if show_new_branch_button? - - = render 'projects/issues/discussion' - -= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees += render 'projects/issuable/show', issuable: @issue diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml new file mode 100644 index 00000000000..7c1ec332ba4 --- /dev/null +++ b/app/views/shared/issue_type/_details_content.html.haml @@ -0,0 +1,31 @@ +- related_branches_path = related_branches_project_issue_path(@project, issuable) + +.issue-details.issuable-details + .detail-page-description.content-block + #js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json} } + .title-container + %h2.title= markdown_field(issuable, :title) + - if issuable.description.present? + .description + .md= markdown_field(issuable, :description) + + = edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') + + = render 'shared/issue_type/sentry_stack_trace', issuable: issuable + + = render 'projects/issues/design_management' + + = render_if_exists 'projects/issues/related_issues' + + #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } + + - if can?(current_user, :download_code, @project) + - add_page_startup_api_call related_branches_path + #related-branches{ data: { url: related_branches_path } } + -# This element is filled in using JavaScript. + + = render 'shared/issue_type/emoji_block', issuable: issuable + + = render 'projects/issues/discussion' + += render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml new file mode 100644 index 00000000000..79ad45e0c6a --- /dev/null +++ b/app/views/shared/issue_type/_details_header.html.haml @@ -0,0 +1,52 @@ +- can_update_issue = can?(current_user, :update_issue, issuable) +- can_reopen_issue = can?(current_user, :reopen_issue, issuable) +- can_report_spam = issuable.submittable_as_spam_by?(current_user) +- can_create_issue = show_new_issue_link?(@project) +- display_issuable_type = issuable_display_type(issuable) +- new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?) + +.detail-page-header + .detail-page-header-body + .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) } + = sprite_icon('mobile-issue-close', css_class: 'gl-display-block gl-display-sm-none!') + .gl-display-none.gl-display-sm-block! + = issue_closed_text(issuable, current_user) + .issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) } + = sprite_icon('issue-open-m', css_class: 'gl-display-block gl-display-sm-none!') + %span.gl-display-none.gl-display-sm-block! + = _('Open') + + .issuable-meta + #js-issuable-header-warnings + = issuable_meta(issuable, @project, display_issuable_type) + + %a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } + = sprite_icon('chevron-double-lg-left') + + .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } + .clearfix.issue-btn-group.dropdown + %button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } + = _('Options') + = icon('caret-down') + .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none + %ul + - unless current_user == issuable.author + %li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable)) + - if can_update_issue + %li= link_to _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(issuable, true)}", title: _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type } + - if can_reopen_issue + %li= link_to _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(issuable, false)}", title: _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type } + - if can_report_spam + %li= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'btn-spam', title: 'Submit as spam' + - if can_create_issue + - if can_update_issue || can_report_spam + %li.divider + %li= link_to _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, new_project_issue_path(@project, new_issuable_params), id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } + + = render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked? + + - if can_report_spam + = link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' + - if can_create_issue + = link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do + = _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type } diff --git a/app/views/shared/issue_type/_emoji_block.html.haml b/app/views/shared/issue_type/_emoji_block.html.haml new file mode 100644 index 00000000000..42d149b2ab3 --- /dev/null +++ b/app/views/shared/issue_type/_emoji_block.html.haml @@ -0,0 +1,9 @@ +.content-block.emoji-block.emoji-block-sticky + .row.gl-m-0.gl-justify-content-space-between + .js-noteable-awards + = render 'award_emoji/awards_block', awardable: issuable, inline: true + .new-branch-col + = render_if_exists "projects/issues/timeline_toggle", issuable: issuable + #js-vue-sort-issue-discussions + #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(issuable), notes_filters: UserPreference.notes_filters.to_json } } + = render 'new_branch' if show_new_branch_button? diff --git a/app/views/shared/issue_type/_sentry_stack_trace.html.haml b/app/views/shared/issue_type/_sentry_stack_trace.html.haml new file mode 100644 index 00000000000..40b29a74b53 --- /dev/null +++ b/app/views/shared/issue_type/_sentry_stack_trace.html.haml @@ -0,0 +1,4 @@ +- return unless issuable.sentry_issue.present? +- add_page_specific_style 'page_bundles/error_tracking_details' + +#js-sentry-error-stack-trace{ data: error_details_data(@project, issuable.sentry_issue.sentry_issue_identifier) } diff --git a/changelogs/unreleased/270106_enable_csp_for_sse.yml b/changelogs/unreleased/270106_enable_csp_for_sse.yml new file mode 100644 index 00000000000..e31fe2e3738 --- /dev/null +++ b/changelogs/unreleased/270106_enable_csp_for_sse.yml @@ -0,0 +1,5 @@ +--- +title: Configure CSP for displaying Youtube videos in the Static Site Editor +merge_request: 45767 +author: +type: fixed diff --git a/changelogs/unreleased/mo-bump-codequality.yml b/changelogs/unreleased/mo-bump-codequality.yml new file mode 100644 index 00000000000..6956b22a0e9 --- /dev/null +++ b/changelogs/unreleased/mo-bump-codequality.yml @@ -0,0 +1,5 @@ +--- +title: Use CodeQuality 0.85.18 in the CI template +merge_request: 46253 +author: +type: changed diff --git a/changelogs/unreleased/sh-pgbouncer-bpass-config.yml b/changelogs/unreleased/sh-pgbouncer-bpass-config.yml new file mode 100644 index 00000000000..5ca15b33163 --- /dev/null +++ b/changelogs/unreleased/sh-pgbouncer-bpass-config.yml @@ -0,0 +1,5 @@ +--- +title: Add environment variables to override backup/restore DB settings +merge_request: 45855 +author: +type: added diff --git a/config/feature_flags/development/oj_json.yml b/config/feature_flags/development/oj_json.yml deleted file mode 100644 index b7e112ae544..00000000000 --- a/config/feature_flags/development/oj_json.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: oj_json -introduced_by_url: -rollout_issue_url: -group: -type: development -default_enabled: true diff --git a/doc/README.md b/doc/README.md index 09638bb4ce8..ddec638505a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ comments: false description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.' --- -<div class="display-none"> +<div class="d-none"> <em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized navigation, discoverability, and readability.</em> </div> diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md index 56b399cec9b..c42d6776341 100644 --- a/doc/api/protected_environments.md +++ b/doc/api/protected_environments.md @@ -96,7 +96,7 @@ POST /projects/:id/protected_environments ``` ```shell -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_environments?name=staging&deploy_access_levels%5B%5D%5Buser_id%5D=1" +curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments" ``` | Attribute | Type | Required | Description | @@ -105,21 +105,22 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla | `name` | string | yes | The name of the environment. | | `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. | -Elements in the `deploy_access_levels` array should take the -form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. +Elements in the `deploy_access_levels` array should be one of `user_id`, `group_id` or +`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or +`{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). Example response: ```json { - "name":"staging", + "name":"production", "deploy_access_levels":[ { - "access_level":null, - "access_level_description":"Administrator", - "user_id":1, - "group_id":null + "access_level":40, + "access_level_description":"protected-access-group", + "user_id":null, + "group_id":9899826 } ] } diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 066a38d68de..8a4cc0c8ff2 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -940,9 +940,7 @@ message. Install the [correct GitLab version](https://packages.gitlab.com/gitlab and then try again. NOTE: **Note:** -There is a known issue with restore not working with `pgbouncer`. The [workaround is to bypass -`pgbouncer` and connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer). -[Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer). +There is a known issue with restore not working with `pgbouncer`. [Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer). ### Restore for Docker image and GitLab Helm chart installations @@ -1039,26 +1037,60 @@ practical use. ## Backup and restore for installations using PgBouncer -PgBouncer can cause the following errors when performing backups and restores: +Do NOT backup or restore GitLab through a PgBouncer connection. These +tasks must [bypass PgBouncer and connect directly to the PostgreSQL primary database node](#bypassing-pgbouncer), +or they will cause a GitLab outage. + +When the GitLab backup or restore task is used with PgBouncer, the +following error message is shown: ```ruby ActiveRecord::StatementInvalid: PG::UndefinedTable ``` -There is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3470) for restore not working -with `pgbouncer`. +This happens because the task uses `pg_dump`, which [sets a null search +path and explicitly includes the schema in every SQL query](https://gitlab.com/gitlab-org/gitlab/-/issues/23211) +to address [CVE-2018-1058](https://www.postgresql.org/about/news/postgresql-103-968-9512-9417-and-9322-released-1834/). + +Since connections are reused with PgBouncer in transaction pooling mode, +PostgreSQL fails to search the default `public` schema. As a result, +this clearing of the search path causes tables and columns to appear +missing. + +### Bypassing PgBouncer + +There are two ways to fix this: + +1. [Use environment variables to override the database settings](#environment-variable-overrides) for the backup task. +1. Reconfigure a node to [connect directly to the PostgreSQL primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer). + +#### Environment variable overrides -To workaround this issue, the GitLab server will need to bypass `pgbouncer` and -[connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer) -to perform the database restore. +By default, GitLab uses the database configuration stored in a +configuration file (`database.yml`). However, you can override the database settings +for the backup and restore task by setting environment +variables that are prefixed with `GITLAB_BACKUP_`: + +- `GITLAB_BACKUP_PGHOST` +- `GITLAB_BACKUP_PGUSER` +- `GITLAB_BACKUP_PGPORT` +- `GITLAB_BACKUP_PGPASSWORD` +- `GITLAB_BACKUP_PGSSLMODE` +- `GITLAB_BACKUP_PGSSLKEY` +- `GITLAB_BACKUP_PGSSLCERT` +- `GITLAB_BACKUP_PGSSLROOTCERT` +- `GITLAB_BACKUP_PGSSLCRL` +- `GITLAB_BACKUP_PGSSLCOMPRESSION` + +For example, to override the database host and port to use 192.168.1.10 +and port 5432 with the Omnibus package: + +```shell +sudo GITLAB_BACKUP_PGHOST=192.168.1.10 GITLAB_BACKUP_PGPORT=5432 /opt/gitlab/bin/gitlab-backup create +``` -There is also a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/23211) -with PostgreSQL 9 and running a database backup through PgBouncer that can cause -an outage to GitLab. If you're still on PostgreSQL 9 and upgrading PostgreSQL isn't -an option, workarounds include having a dedicated application node just for backups, -configured to connect directly the primary database node as noted above. You're -advised to upgrade your PostgreSQL version though, GitLab 11.11 shipped with PostgreSQL -10.7, and that is the recommended version for GitLab 12+. +See the [PostgreSQL documentation](https://www.postgresql.org/docs/12/libpq-envars.html) +for more details on what these parameters do. ## Additional notes diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index 9508407ccae..0d619659bd1 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -30,6 +30,7 @@ Docker image with the fuzz engine to run your app. | Swift | [libfuzzer](https://github.com/apple/swift/blob/master/docs/libFuzzerIntegration.md) | [swift-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/swift-fuzzing-example) | | Rust | [cargo-fuzz (libFuzzer support)](https://github.com/rust-fuzz/cargo-fuzz) | [rust-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/rust-fuzzing-example) | | Java | [JQF](https://github.com/rohanpadhye/JQF) | [java-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) | +| Java | [javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) | ## Configuration diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 53ba3d01b3e..8e999de8f4c 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -30,31 +30,33 @@ The Package Registry supports the following formats: You can also use the [API](../../api/packages.md) to administer the Package Registry. -The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. -It's built on open source software and completely integrated within GitLab. -Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to -manage the registry across groups and projects. +## Accepting contributions -The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages. - -## Suggested contributions - -Consider contributing to GitLab. This [development documentation](../../development/packages.md) will +The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages.md) will guide you through the process. Or check out how other members of the community are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17417) or [Terraform](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834). -| Format | Use case | +| Format | Status | | ------ | ------ | -| [Cargo](https://gitlab.com/gitlab-org/gitlab/-/issues/33060) | Cargo is the Rust package manager. Build, publish and share Rust packages | -| [Chef](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) | Configuration management with Chef using all the benefits of a repository manager. | -| [CocoaPods](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) | Speed up development with Xcode and CocoaPods. | -| [Conda](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | Secure and private local Conda repositories. | -| [CRAN](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | Deploy and resolve CRAN packages for the R language. | -| [Debian](https://gitlab.com/gitlab-org/gitlab/-/issues/5835) | Host and provision Debian packages. | -| [Opkg](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | Optimize your work with OpenWrt using Opkg repositories. | -| [P2](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | Host all your Eclipse plugins in your own GitLab P2 repository. | -| [Puppet](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | Configuration management meets repository management with Puppet repositories. | -| [RPM](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) | Distribute RPMs directly from GitLab. | -| [RubyGems](https://gitlab.com/gitlab-org/gitlab/-/issues/803) | Use GitLab to host your own gems. | -| [SBT](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) | Resolve dependencies from and deploy build output to SBT repositories when running SBT builds. | -| [Vagrant](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) | Securely host your Vagrant boxes in local repositories. | +| Chef | [#36889](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) | +| CocoaPods | [#36890](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) | +| CocoaPods | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | +| Conda | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | +| CRAN | [#36892](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | +| Debian | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44746) | +| Opkg | [#36894](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | +| P2 | [#36895](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | +| Puppet | [#36897](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | +| RPM | [#5932](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) | +| RubyGems | [#803](https://gitlab.com/gitlab-org/gitlab/-/issues/803) | +| SBT | [#36898](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) | +| Terraform | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834) | +| Vagrant | [#36899](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) | + +## Container Registry + +The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. It's built on open source software and completely integrated within GitLab. Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to manage the registry across groups and projects. + +## Dependency Proxy + +The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages. diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 851445f703d..0429d9496d6 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -140,7 +140,14 @@ module Backup 'sslcrl' => 'PGSSLCRL', 'sslcompression' => 'PGSSLCOMPRESSION' } - args.each { |opt, arg| ENV[arg] = config[opt].to_s if config[opt] } + args.each do |opt, arg| + # This enables the use of different PostgreSQL settings in + # case PgBouncer is used. PgBouncer clears the search path, + # which wreaks havoc on Rails if connections are reused. + override = "GITLAB_BACKUP_#{arg}" + val = ENV[override].presence || config[opt].to_s.presence + ENV[arg] = val if val + end end def report_success(success) diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index ec33020205b..054ec03cdf7 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -7,7 +7,7 @@ code_quality: variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" - CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18" needs: [] script: - export SOURCE_CODE=$PWD diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb index 29cfec443e8..8565f664cd4 100644 --- a/lib/gitlab/json.rb +++ b/lib/gitlab/json.rb @@ -67,15 +67,6 @@ module Gitlab ::JSON.pretty_generate(object, opts) end - # Feature detection for using Oj instead of the `json` gem. - # - # @return [Boolean] - def enable_oj? - return false unless feature_table_exists? - - Feature.enabled?(:oj_json, default_enabled: true) - end - private # Convert JSON string into Ruby through toggleable adapters. @@ -91,11 +82,7 @@ module Gitlab def adapter_load(string, *args, **opts) opts = standardize_opts(opts) - if enable_oj? - Oj.load(string, opts) - else - ::JSON.parse(string, opts) - end + Oj.load(string, opts) rescue Oj::ParseError, Encoding::UndefinedConversionError => ex raise parser_error.new(ex) end @@ -120,11 +107,7 @@ module Gitlab # # @return [String] def adapter_dump(object, *args, **opts) - if enable_oj? - Oj.dump(object, opts) - else - ::JSON.dump(object, *args) - end + Oj.dump(object, opts) end # Generates JSON for an object but with fewer options, using toggleable adapters. @@ -135,11 +118,7 @@ module Gitlab def adapter_generate(object, opts = {}) opts = standardize_opts(opts) - if enable_oj? - Oj.generate(object, opts) - else - ::JSON.generate(object, opts) - end + Oj.generate(object, opts) end # Take a JSON standard options hash and standardize it to work across adapters @@ -149,11 +128,8 @@ module Gitlab # @return [Hash] def standardize_opts(opts) opts ||= {} - - if enable_oj? - opts[:mode] = :rails - opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names] - end + opts[:mode] = :rails + opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names] opts end @@ -213,7 +189,7 @@ module Gitlab # @param object [Object] # @return [String] def self.call(object, env = nil) - if Gitlab::Json.enable_oj? && Feature.enabled?(:grape_gitlab_json, default_enabled: true) + if Feature.enabled?(:grape_gitlab_json, default_enabled: true) Gitlab::Json.dump(object) else Grape::Formatter::Json.call(object, env) diff --git a/lib/gitlab/repository_size_checker.rb b/lib/gitlab/repository_size_checker.rb index 03d9f961dd9..dbfec77cb18 100644 --- a/lib/gitlab/repository_size_checker.rb +++ b/lib/gitlab/repository_size_checker.rb @@ -32,7 +32,7 @@ module Gitlab def changes_will_exceed_size_limit?(change_size) return false unless enabled? - change_size > limit || exceeded_size(change_size) > 0 + above_size_limit? || exceeded_size(change_size) > 0 end # @param change_size [int] in bytes diff --git a/lib/gitlab/repository_url_builder.rb b/lib/gitlab/repository_url_builder.rb index 2b88af1f77c..a2d0d50d20b 100644 --- a/lib/gitlab/repository_url_builder.rb +++ b/lib/gitlab/repository_url_builder.rb @@ -4,9 +4,6 @@ module Gitlab module RepositoryUrlBuilder class << self def build(path, protocol: :ssh) - # TODO: See https://gitlab.com/gitlab-org/gitlab/-/issues/213021 - path = path.sub('@snippets', 'snippets') - case protocol when :ssh ssh_url(path) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6def372b3e7..efb09f2f871 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17716,6 +17716,9 @@ msgstr "" msgid "New" msgstr "" +msgid "New %{display_issuable_type}" +msgstr "" + msgid "New Application" msgstr "" @@ -25457,6 +25460,9 @@ msgstr "" msgid "Submit a review" msgstr "" +msgid "Submit as spam" +msgstr "" + msgid "Submit changes" msgstr "" diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb new file mode 100644 index 00000000000..9615abebfde --- /dev/null +++ b/spec/features/incidents/user_views_incident_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "User views incident" do + let_it_be(:project) { create(:project_empty_repo, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:incident) { create(:incident, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) } + let_it_be(:note) { create(:note, noteable: incident, project: project, author: user) } + + before_all do + project.add_developer(user) + end + + before do + sign_in(user) + + visit(project_issues_incident_path(project, incident)) + end + + it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") } + + it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet' + + it 'shows the merge request and incident actions', :aggregate_failures do + expect(page).to have_link('New incident') + expect(page).to have_button('Create merge request') + expect(page).to have_link('Close incident') + end + + context 'when the project is archived' do + before do + project.update!(archived: true) + visit(project_issues_incident_path(project, incident)) + end + + it 'hides the merge request and incident actions', :aggregate_failures do + expect(page).not_to have_link('New incident') + expect(page).not_to have_button('Create merge request') + expect(page).not_to have_link('Close incident') + end + end + + describe 'user status' do + subject { visit(project_issues_incident_path(project, incident)) } + + context 'when showing status of the author of the incident' do + it_behaves_like 'showing user status' do + let(:user_with_status) { user } + end + end + + context 'when showing status of a user who commented on an incident', :js do + it_behaves_like 'showing user status' do + let(:user_with_status) { user } + end + end + + context 'when status message has an emoji', :js do + let_it_be(:message) { 'My status with an emoji' } + let_it_be(:message_emoji) { 'basketball' } + let_it_be(:status) { create(:user_status, user: user, emoji: 'smirk', message: "#{message} :#{message_emoji}:") } + + it 'correctly renders the emoji' do + wait_for_requests + + tooltip_span = page.first(".user-status-emoji[title^='#{message}']") + tooltip_span.hover + + wait_for_requests + + tooltip = page.find('.tooltip .tooltip-inner') + + page.within(tooltip) do + expect(page).to have_emoji(message_emoji) + end + end + end + end +end diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb index 7db72f2cd05..bb9bce9eccf 100644 --- a/spec/features/issues/user_interacts_with_awards_spec.rb +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -68,7 +68,7 @@ RSpec.describe 'User interacts with awards' do page.within('.awards') do expect(page).to have_selector('.js-emoji-btn') expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page).to have_css(".js-emoji-btn.active[title='You']") expect do page.find('.js-emoji-btn.active').click diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 5cbfacf4e48..c2d5b0acda6 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'User uses header search field', :js do wait_for_all_requests end - it 'shows the category search dropdown' do + it 'shows the category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) end end @@ -44,7 +44,7 @@ RSpec.describe 'User uses header search field', :js do page.find('#search').click end - it 'shows category search dropdown' do + it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) end @@ -104,7 +104,7 @@ RSpec.describe 'User uses header search field', :js do let(:scope_name) { 'All GitLab' } end - it 'displays search options' do + it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) diff --git a/spec/features/static_site_editor_spec.rb b/spec/features/static_site_editor_spec.rb index 03085917d67..a47579582e2 100644 --- a/spec/features/static_site_editor_spec.rb +++ b/spec/features/static_site_editor_spec.rb @@ -73,4 +73,44 @@ RSpec.describe 'Static Site Editor' do expect(node['data-static-site-generator']).to eq('middleman') end end + + describe 'Static Site Editor Content Security Policy' do + subject { response_headers['Content-Security-Policy'] } + + context 'when no global CSP config exists' do + before do + expect_next_instance_of(Projects::StaticSiteEditorController) do |controller| + expect(controller).to receive(:current_content_security_policy) + .and_return(ActionDispatch::ContentSecurityPolicy.new) + end + end + + it 'does not add CSP directives' do + visit sse_path + + is_expected.to be_blank + end + end + + context 'when a global CSP config exists' do + let_it_be(:cdn_url) { 'https://some-cdn.test' } + let_it_be(:youtube_url) { 'https://www.youtube.com' } + + before do + csp = ActionDispatch::ContentSecurityPolicy.new do |p| + p.frame_src :self, cdn_url + end + + expect_next_instance_of(Projects::StaticSiteEditorController) do |controller| + expect(controller).to receive(:current_content_security_policy).and_return(csp) + end + end + + it 'appends youtube to the CSP frame-src policy' do + visit sse_path + + is_expected.to eql("frame-src 'self' #{cdn_url} #{youtube_url}") + end + end + end end diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js index 7fd6a9e7b87..c6a9c911ccf 100644 --- a/spec/frontend/awards_handler_spec.js +++ b/spec/frontend/awards_handler_spec.js @@ -169,29 +169,6 @@ describe('AwardsHandler', () => { }); }); - describe('::userAuthored', () => { - it('should update tooltip to user authored title', () => { - const $votesBlock = $('.js-awards-block').eq(0); - const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); - $thumbsUpEmoji.attr('data-title', 'sam'); - awardsHandler.userAuthored($thumbsUpEmoji); - - expect($thumbsUpEmoji.data('originalTitle')).toBe( - 'You cannot vote on your own issue, MR and note', - ); - }); - - it('should restore tooltip back to initial vote list', () => { - const $votesBlock = $('.js-awards-block').eq(0); - const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); - $thumbsUpEmoji.attr('data-title', 'sam'); - awardsHandler.userAuthored($thumbsUpEmoji); - jest.advanceTimersByTime(2801); - - expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); - }); - }); - describe('::getAwardUrl', () => { it('returns the url for request', () => { expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji'); diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7a243573b6d..0e3752f220e 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -345,6 +345,24 @@ RSpec.describe IssuablesHelper do end end + describe '#issuable_display_type' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :issuable_display_type) do + :issue | 'issue' + :incident | 'incident' + :merge_request | 'merge request' + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { helper.issuable_display_type(issuable) } + + it { is_expected.to eq(issuable_display_type) } + end + end + describe '#sidebar_milestone_tooltip_label' do it 'escapes HTML in the milestone title' do milestone = build(:milestone, title: '<img onerror=alert(1)>') diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb index fccd6db0018..2bce4cab679 100644 --- a/spec/lib/backup/database_spec.rb +++ b/spec/lib/backup/database_spec.rb @@ -48,5 +48,26 @@ RSpec.describe Backup::Database do expect(output).to include(visible_error) end end + + context 'with PostgreSQL settings defined in the environment' do + let(:cmd) { %W[#{Gem.ruby} -e] + ["$stderr.puts ENV.to_h.select { |k, _| k.start_with?('PG') }"] } + let(:config) { YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))['test'] } + + before do + stub_const 'ENV', ENV.to_h.merge({ + 'GITLAB_BACKUP_PGHOST' => 'test.example.com', + 'PGPASSWORD' => 'donotchange' + }) + end + + it 'overrides default config values' do + subject.restore + + expect(output).to include(%("PGHOST"=>"test.example.com")) + expect(output).to include(%("PGPASSWORD"=>"donotchange")) + expect(output).to include(%("PGPORT"=>"#{config['port']}")) if config['port'] + expect(output).to include(%("PGUSER"=>"#{config['username']}")) if config['username'] + end + end end end diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb index 0402296a3a8..59ec94f2855 100644 --- a/spec/lib/gitlab/json_spec.rb +++ b/spec/lib/gitlab/json_spec.rb @@ -7,342 +7,306 @@ RSpec.describe Gitlab::Json do stub_feature_flags(json_wrapper_legacy_mode: true) end - shared_examples "json" do - describe ".parse" do - context "legacy_mode is disabled by default" do - it "parses an object" do - expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" }) - end - - it "parses an array" do - expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }]) - end - - it "parses a string" do - expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo") - end - - it "parses a true bool" do - expect(subject.parse("true", legacy_mode: false)).to be(true) - end - - it "parses a false bool" do - expect(subject.parse("false", legacy_mode: false)).to be(false) - end + describe ".parse" do + context "legacy_mode is disabled by default" do + it "parses an object" do + expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" }) end - context "legacy_mode is enabled" do - it "parses an object" do - expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) - end - - it "parses an array" do - expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) - end - - it "raises an error on a string" do - expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError) - end - - it "raises an error on a true bool" do - expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError) - end - - it "raises an error on a false bool" do - expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError) - end + it "parses an array" do + expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }]) end - context "feature flag is disabled" do - before do - stub_feature_flags(json_wrapper_legacy_mode: false) - end - - it "parses an object" do - expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) - end - - it "parses an array" do - expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) - end - - it "parses a string" do - expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo") - end + it "parses a string" do + expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo") + end - it "parses a true bool" do - expect(subject.parse("true", legacy_mode: true)).to be(true) - end + it "parses a true bool" do + expect(subject.parse("true", legacy_mode: false)).to be(true) + end - it "parses a false bool" do - expect(subject.parse("false", legacy_mode: true)).to be(false) - end + it "parses a false bool" do + expect(subject.parse("false", legacy_mode: false)).to be(false) end end - describe ".parse!" do - context "legacy_mode is disabled by default" do - it "parses an object" do - expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" }) - end + context "legacy_mode is enabled" do + it "parses an object" do + expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) + end - it "parses an array" do - expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }]) - end + it "parses an array" do + expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) + end - it "parses a string" do - expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo") - end + it "raises an error on a string" do + expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError) + end - it "parses a true bool" do - expect(subject.parse!("true", legacy_mode: false)).to be(true) - end + it "raises an error on a true bool" do + expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError) + end - it "parses a false bool" do - expect(subject.parse!("false", legacy_mode: false)).to be(false) - end + it "raises an error on a false bool" do + expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError) end + end - context "legacy_mode is enabled" do - it "parses an object" do - expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) - end + context "feature flag is disabled" do + before do + stub_feature_flags(json_wrapper_legacy_mode: false) + end - it "parses an array" do - expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) - end + it "parses an object" do + expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) + end - it "raises an error on a string" do - expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError) - end + it "parses an array" do + expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) + end - it "raises an error on a true bool" do - expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError) - end + it "parses a string" do + expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo") + end - it "raises an error on a false bool" do - expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError) - end + it "parses a true bool" do + expect(subject.parse("true", legacy_mode: true)).to be(true) end - context "feature flag is disabled" do - before do - stub_feature_flags(json_wrapper_legacy_mode: false) - end + it "parses a false bool" do + expect(subject.parse("false", legacy_mode: true)).to be(false) + end + end + end - it "parses an object" do - expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) - end + describe ".parse!" do + context "legacy_mode is disabled by default" do + it "parses an object" do + expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" }) + end - it "parses an array" do - expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) - end + it "parses an array" do + expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }]) + end - it "parses a string" do - expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo") - end + it "parses a string" do + expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo") + end - it "parses a true bool" do - expect(subject.parse!("true", legacy_mode: true)).to be(true) - end + it "parses a true bool" do + expect(subject.parse!("true", legacy_mode: false)).to be(true) + end - it "parses a false bool" do - expect(subject.parse!("false", legacy_mode: true)).to be(false) - end + it "parses a false bool" do + expect(subject.parse!("false", legacy_mode: false)).to be(false) end end - describe ".dump" do - it "dumps an object" do - expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}') + context "legacy_mode is enabled" do + it "parses an object" do + expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) end - it "dumps an array" do - expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]') + it "parses an array" do + expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) end - it "dumps a string" do - expect(subject.dump("foo")).to eq('"foo"') + it "raises an error on a string" do + expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError) end - it "dumps a true bool" do - expect(subject.dump(true)).to eq("true") + it "raises an error on a true bool" do + expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError) end - it "dumps a false bool" do - expect(subject.dump(false)).to eq("false") + it "raises an error on a false bool" do + expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError) end end - describe ".generate" do - let(:obj) do - { test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] } + context "feature flag is disabled" do + before do + stub_feature_flags(json_wrapper_legacy_mode: false) end - it "generates JSON" do - expected_string = <<~STR.chomp - {"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]} - STR + it "parses an object" do + expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) + end - expect(subject.generate(obj)).to eq(expected_string) + it "parses an array" do + expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) end - it "allows you to customise the output" do - opts = { - indent: " ", - space: " ", - space_before: " ", - object_nl: "\n", - array_nl: "\n" - } + it "parses a string" do + expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo") + end - json = subject.generate(obj, opts) - - expected_string = <<~STR.chomp - { - "test" : true, - "foo.bar" : "baz", - "is_json" : 1, - "some" : [ - 1, - 2, - 3 - ] - } - STR + it "parses a true bool" do + expect(subject.parse!("true", legacy_mode: true)).to be(true) + end - expect(json).to eq(expected_string) + it "parses a false bool" do + expect(subject.parse!("false", legacy_mode: true)).to be(false) end end + end - describe ".pretty_generate" do - let(:obj) do - { - test: true, - "foo.bar" => "baz", - is_json: 1, - some: [1, 2, 3], - more: { test: true }, - multi_line_empty_array: [], - multi_line_empty_obj: {} - } - end + describe ".dump" do + it "dumps an object" do + expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}') + end - it "generates pretty JSON" do - expected_string = <<~STR.chomp - { - "test": true, - "foo.bar": "baz", - "is_json": 1, - "some": [ - 1, - 2, - 3 - ], - "more": { - "test": true - }, - "multi_line_empty_array": [ - - ], - "multi_line_empty_obj": { - } - } - STR + it "dumps an array" do + expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]') + end - expect(subject.pretty_generate(obj)).to eq(expected_string) - end + it "dumps a string" do + expect(subject.dump("foo")).to eq('"foo"') + end - it "allows you to customise the output" do - opts = { - space_before: " " - } + it "dumps a true bool" do + expect(subject.dump(true)).to eq("true") + end - json = subject.pretty_generate(obj, opts) - - expected_string = <<~STR.chomp - { - "test" : true, - "foo.bar" : "baz", - "is_json" : 1, - "some" : [ - 1, - 2, - 3 - ], - "more" : { - "test" : true - }, - "multi_line_empty_array" : [ - - ], - "multi_line_empty_obj" : { - } - } - STR + it "dumps a false bool" do + expect(subject.dump(false)).to eq("false") + end + end - expect(json).to eq(expected_string) - end + describe ".generate" do + let(:obj) do + { test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] } end - context "the feature table is missing" do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false) - end + it "generates JSON" do + expected_string = <<~STR.chomp + {"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]} + STR + + expect(subject.generate(obj)).to eq(expected_string) + end - it "skips legacy mode handling" do - expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true) + it "allows you to customise the output" do + opts = { + indent: " ", + space: " ", + space_before: " ", + object_nl: "\n", + array_nl: "\n" + } - subject.send(:handle_legacy_mode!, {}) - end + json = subject.generate(obj, opts) - it "skips oj feature detection" do - expect(Feature).not_to receive(:enabled?).with(:oj_json, default_enabled: true) + expected_string = <<~STR.chomp + { + "test" : true, + "foo.bar" : "baz", + "is_json" : 1, + "some" : [ + 1, + 2, + 3 + ] + } + STR - subject.send(:enable_oj?) - end + expect(json).to eq(expected_string) end + end - context "the database is missing" do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad) - end + describe ".pretty_generate" do + let(:obj) do + { + test: true, + "foo.bar" => "baz", + is_json: 1, + some: [1, 2, 3], + more: { test: true }, + multi_line_empty_array: [], + multi_line_empty_obj: {} + } + end - it "still parses json" do - expect(subject.parse("{}")).to eq({}) - end + it "generates pretty JSON" do + expected_string = <<~STR.chomp + { + "test": true, + "foo.bar": "baz", + "is_json": 1, + "some": [ + 1, + 2, + 3 + ], + "more": { + "test": true + }, + "multi_line_empty_array": [ + + ], + "multi_line_empty_obj": { + } + } + STR - it "still generates json" do - expect(subject.dump({})).to eq("{}") - end + expect(subject.pretty_generate(obj)).to eq(expected_string) + end + + it "allows you to customise the output" do + opts = { + space_before: " " + } + + json = subject.pretty_generate(obj, opts) + + expected_string = <<~STR.chomp + { + "test" : true, + "foo.bar" : "baz", + "is_json" : 1, + "some" : [ + 1, + 2, + 3 + ], + "more" : { + "test" : true + }, + "multi_line_empty_array" : [ + + ], + "multi_line_empty_obj" : { + } + } + STR + + expect(json).to eq(expected_string) end end - context "oj gem" do + context "the feature table is missing" do before do - stub_feature_flags(oj_json: true) + allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false) end - it_behaves_like "json" + it "skips legacy mode handling" do + expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true) - describe "#enable_oj?" do - it "returns true" do - expect(subject.enable_oj?).to be(true) - end + subject.send(:handle_legacy_mode!, {}) end end - context "json gem" do + context "the database is missing" do before do - stub_feature_flags(oj_json: false) + allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad) end - it_behaves_like "json" + it "still parses json" do + expect(subject.parse("{}")).to eq({}) + end - describe "#enable_oj?" do - it "returns false" do - expect(subject.enable_oj?).to be(false) - end + it "still generates json" do + expect(subject.dump({})).to eq("{}") end end @@ -353,47 +317,25 @@ RSpec.describe Gitlab::Json do let(:env) { {} } let(:result) { "{\"test\":true}" } - context "oj is enabled" do + context "grape_gitlab_json flag is enabled" do before do - stub_feature_flags(oj_json: true) + stub_feature_flags(grape_gitlab_json: true) end - context "grape_gitlab_json flag is enabled" do - before do - stub_feature_flags(grape_gitlab_json: true) - end - - it "generates JSON" do - expect(subject).to eq(result) - end - - it "uses Gitlab::Json" do - expect(Gitlab::Json).to receive(:dump).with(obj) - - subject - end + it "generates JSON" do + expect(subject).to eq(result) end - context "grape_gitlab_json flag is disabled" do - before do - stub_feature_flags(grape_gitlab_json: false) - end - - it "generates JSON" do - expect(subject).to eq(result) - end + it "uses Gitlab::Json" do + expect(Gitlab::Json).to receive(:dump).with(obj) - it "uses Grape::Formatter::Json" do - expect(Grape::Formatter::Json).to receive(:call).with(obj, env) - - subject - end + subject end end - context "oj is disabled" do + context "grape_gitlab_json flag is disabled" do before do - stub_feature_flags(oj_json: false) + stub_feature_flags(grape_gitlab_json: false) end it "generates JSON" do diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb index 234f6e4b4b5..212605445ff 100644 --- a/spec/models/personal_snippet_spec.rb +++ b/spec/models/personal_snippet_spec.rb @@ -20,9 +20,8 @@ RSpec.describe PersonalSnippet do it_behaves_like 'model with repository' do let_it_be(:container) { create(:personal_snippet, :repository) } let(:stubbed_container) { build_stubbed(:personal_snippet) } - let(:expected_full_path) { "@snippets/#{container.id}" } + let(:expected_full_path) { "snippets/#{container.id}" } let(:expected_web_url_path) { "-/snippets/#{container.id}" } - let(:expected_repo_url_path) { "snippets/#{container.id}" } end describe '#parent_user' do diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index 3bcbf6b9e1b..3d1c87771f3 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -36,8 +36,7 @@ RSpec.describe ProjectSnippet do it_behaves_like 'model with repository' do let_it_be(:container) { create(:project_snippet, :repository) } let(:stubbed_container) { build_stubbed(:project_snippet) } - let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" } + let(:expected_full_path) { "#{container.project.full_path}/snippets/#{container.id}" } let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" } - let(:expected_repo_url_path) { "#{container.project.full_path}/snippets/#{container.id}" } end end |