diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5bb29a7aa3..28ac2532f1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ workflow: # If `$FORCE_GITLAB_CI` is set, create a pipeline. - if: '$FORCE_GITLAB_CI' variables: - RUBY_VERSION: "3.0" + RUBY_VERSION: "2.7" # As part of the process of creating RCs automatically, we update stable # branches with the changes of the most recent production deployment. The # merge requests used for this merge a branch release-tools/X into a stable @@ -46,10 +46,10 @@ workflow: # For (detached) merge request pipelines. - if: '$CI_MERGE_REQUEST_IID' variables: - RUBY_VERSION: "3.0" - OMNIBUS_GITLAB_RUBY3_BUILD: "true" - OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY3" - PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline' + RUBY_VERSION: "2.7" + OMNIBUS_GITLAB_RUBY2_BUILD: "true" + OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY2" + PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline' # For the scheduled pipelines, we set specific variables. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"' variables: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6071648bf6..bf4ae0fdef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.8.5 (2023-03-30) + +### Security (16 changes) + +- [Fix rubocop offenses in lib/gitlab/url_sanitizer.rb](gitlab-org/security/gitlab@ddc04cf7059e411e20033b95e1297381d64d4b22) ([merge request](gitlab-org/security/gitlab!3175)) +- [Add checks to remove open redirects from Observability URL](gitlab-org/security/gitlab@a22ce3851128eb900dbabe9e38c07889967a2915) ([merge request](gitlab-org/security/gitlab!3032)) +- [Redirect to tree from project root on ref collision](gitlab-org/security/gitlab@fad24ae9d8fa0e7bd9eff0c9e6914c8267451b4d) ([merge request](gitlab-org/security/gitlab!3134)) +- [Fixes soft email confirmation alert vulnerability](gitlab-org/security/gitlab@85be0fbfc98cdb774d68070479e35be22f6ba40a) ([merge request](gitlab-org/security/gitlab!3125)) +- [Restrict Prometheus API access on public projects](gitlab-org/security/gitlab@2df2fa2dc4b9015d044d0ddc5d26e17e9e5f85c0) ([merge request](gitlab-org/security/gitlab!3164)) +- [Verify that users have access to the parent of the fork](gitlab-org/security/gitlab@53f7f06843eea4d666d361f5a1d349bd1e3f4312) ([merge request](gitlab-org/security/gitlab!3085)) +- [Protect webhook secrets by resetting url_variables](gitlab-org/security/gitlab@9fa9dbff463f6015ffaf8d082db3d41ae623763e) ([merge request](gitlab-org/security/gitlab!3141)) +- [Replace Unicode space chars with spaces](gitlab-org/security/gitlab@20d77d4d680d13f916fb69de0d79802753421c8f) ([merge request](gitlab-org/security/gitlab!3137)) +- [Check access to parent when creating and updating epics](gitlab-org/security/gitlab@0fed113756b27a3a078f87f29711b225e1ed4cce) ([merge request](gitlab-org/security/gitlab!3150)) +- [Improve Gitlab::UrlSanitizer regex to match more URIs](gitlab-org/security/gitlab@2285088f37aca877b1dcd59c728cdf33171b30cb) ([merge request](gitlab-org/security/gitlab!3109)) +- [Check access to target project before looking for branch](gitlab-org/security/gitlab@37b8d855d87c88170322e6a6d4c285fee6c6cb64) ([merge request](gitlab-org/security/gitlab!3038)) +- [Fix the potential leak of internal notes](gitlab-org/security/gitlab@66f8cc2eb13509397b980d53a4b67ca03d8903f7) ([merge request](gitlab-org/security/gitlab!3121)) +- [Filter namespace environments by feature visibility](gitlab-org/security/gitlab@e1859de393b4794e1356d6318e56ede4b557c059) ([merge request](gitlab-org/security/gitlab!3112)) +- [Check access to reorder issues in epic tree](gitlab-org/security/gitlab@13f9c6231cea956f73355c5b5b820163f523e7d8) ([merge request](gitlab-org/security/gitlab!3100)) +- [Fix security report authorization](gitlab-org/security/gitlab@19baab85c7a5a64a09e3e4808e8550fc72e18323) ([merge request](gitlab-org/security/gitlab!3105)) +- [Prevent XSS attack in "Maximum page reached" page](gitlab-org/security/gitlab@be5491c5db05161e4b14d53900dd19b66848de48) ([merge request](gitlab-org/security/gitlab!3131)) + ## 15.8.4 (2023-03-02) ### Security (12 changes) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index da467831eb..317374c81d 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -15.8.4 \ No newline at end of file +15.8.5 \ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index da467831eb..317374c81d 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -15.8.4 \ No newline at end of file +15.8.5 \ No newline at end of file diff --git a/VERSION b/VERSION index da467831eb..317374c81d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15.8.4 \ No newline at end of file +15.8.5 \ No newline at end of file diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js index 704d85cf22..d5d46c10ef 100644 --- a/app/assets/javascripts/behaviors/markdown/render_observability.js +++ b/app/assets/javascripts/behaviors/markdown/render_observability.js @@ -7,23 +7,36 @@ export function getFrameSrc(url) { } const mountVueComponent = (element) => { - const url = [element.dataset.frameUrl]; + const { frameUrl, observabilityUrl } = element.dataset; - return new Vue({ - el: element, - render(h) { - return h('iframe', { - style: { - height: '366px', - width: '768px', - }, - attrs: { - src: getFrameSrc(url), - frameBorder: '0', - }, - }); - }, - }); + try { + if ( + !observabilityUrl || + !frameUrl || + new URL(frameUrl)?.host !== new URL(observabilityUrl).host + ) + return; + + // eslint-disable-next-line no-new + new Vue({ + el: element, + render(h) { + return h('iframe', { + style: { + height: '366px', + width: '768px', + }, + attrs: { + src: getFrameSrc(frameUrl), + frameBorder: '0', + }, + }); + }, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }; export default function renderObservability(elements) { diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 4c9eb830ff..27659348dc 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -118,9 +118,10 @@ export default class Project { const urlParams = { [fieldName]: ref }; if (params.group === BRANCH_GROUP_NAME) { urlParams.ref_type = BRANCH_REF_TYPE; - } else { + } else if (params.group === TAG_GROUP_NAME) { urlParams.ref_type = TAG_REF_TYPE; } + link.href = mergeUrlParams(urlParams, linkTarget); } diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index e5d22f50d7..0f6b159ffc 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; import Vuex from 'vuex'; import { parseBoolean } from '~/lib/utils/common_utils'; -import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility'; +import { joinPaths, escapeFileUrl, visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; import PerformancePlugin from '~/performance/vue_performance_plugin'; @@ -119,7 +119,7 @@ export default function setupVueRepositoryList() { if (!refSwitcherEl) return false; - const { projectId, projectRootPath } = refSwitcherEl.dataset; + const { projectId, projectRootPath, refType } = refSwitcherEl.dataset; return new Vue({ el: refSwitcherEl, @@ -127,11 +127,12 @@ export default function setupVueRepositoryList() { return createElement(RefSelector, { props: { projectId, - value: ref, + value: refType ? joinPaths('refs', refType, ref) : ref, + useSymbolicRefNames: true, }, on: { input(selectedRef) { - visitUrl(generateRefDestinationPath(projectRootPath, selectedRef)); + visitUrl(generateRefDestinationPath(projectRootPath, ref, selectedRef)); }, }, }); diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js index f296b5e9b4..fc0d521ad5 100644 --- a/app/assets/javascripts/repository/utils/ref_switcher_utils.js +++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js @@ -15,22 +15,30 @@ const NAMESPACE_TARGET_REGEX = /(\/-\/(blob|tree))\/.*?\/(.*)/; * @param {string} projectRootPath - The root path for a project. * @param {string} selectedRef - The selected ref from the ref dropdown. */ -export function generateRefDestinationPath(projectRootPath, selectedRef) { - const currentPath = window.location.pathname; - const encodedHash = '%23'; + +export function generateRefDestinationPath(projectRootPath, ref, selectedRef) { + const url = new URL(window.location.href); + const currentPath = url.pathname; + let refType = null; let namespace = '/-/tree'; let target; + let actualRef = selectedRef; + + const matches = selectedRef.match(/^refs\/(heads|tags)\/(.+)/); + if (matches) { + [, refType, actualRef] = matches; + } + if (refType) { + url.searchParams.set('ref_type', refType); + } else { + url.searchParams.delete('ref_type'); + } + const match = NAMESPACE_TARGET_REGEX.exec(currentPath); if (match) { [, namespace, , target] = match; } + url.pathname = joinPaths(projectRootPath, namespace, actualRef, target); - const destinationPath = joinPaths( - projectRootPath, - namespace, - encodeURI(selectedRef).replace(/#/g, encodedHash), - target, - ); - - return `${destinationPath}${window.location.hash}`; + return url.toString(); } diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb index ec5140bf22..2711c82327 100644 --- a/app/controllers/concerns/confirm_email_warning.rb +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module ConfirmEmailWarning + include Gitlab::Utils::StrongMemoize extend ActiveSupport::Concern included do @@ -17,11 +18,9 @@ module ConfirmEmailWarning return unless current_user return if current_user.confirmed? - email = current_user.unconfirmed_email || current_user.email - flash.now[:warning] = format( confirm_warning_message, - email: email, + email: email_to_display, resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post), update_link: view_context.link_to(_('Update it'), profile_path) ).html_safe @@ -29,7 +28,16 @@ module ConfirmEmailWarning private + def email + current_user.unconfirmed_email || current_user.email + end + strong_memoize_attr :email + def confirm_warning_message _("Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.") end + + def email_to_display + html_escape(email) + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 4eda76f4f2..dd2f980e88 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy] before_action :commit, except: [:new, :create] + before_action :check_for_ambiguous_ref, only: [:show] before_action :blob, except: [:new, :create] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] @@ -145,6 +146,15 @@ class Projects::BlobController < Projects::ApplicationController end end + def check_for_ambiguous_ref + @ref_type = ref_type + + if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref) + branch = @project.repository.find_branch(@ref) + redirect_to project_blob_path(@project, File.join(branch.target, @path)) + end + end + def commit @commit ||= @repository.commit(@ref) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 8ac6d872aa..0b52d03ab9 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -22,7 +22,7 @@ class Projects::RefsController < Projects::ApplicationController when "tree" project_tree_path(@project, @id) when "blob" - project_blob_path(@project, @id) + project_blob_path(@project, @id, ref_type: ref_type) when "graph" if Feature.enabled?(:use_ref_type_parameter, @project) project_network_path(@project, @id, ref_type: ref_type) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 737a629043..1f7912e15d 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -28,6 +28,15 @@ class Projects::TreeController < Projects::ApplicationController def show return render_404 unless @commit + @ref_type = ref_type + if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref) + branch = @project.repository.find_branch(@ref) + if branch + redirect_to project_tree_path(@project, branch.target) + return + end + end + if tree.entries.empty? if @repository.blob_at(@commit.id, @path) redirect_to project_blob_path(@project, File.join(@ref, @path)) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ee2c268ff3..ed7c163c10 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -172,11 +172,19 @@ class ProjectsController < Projects::ApplicationController flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } end + if ambiguous_ref?(@project, @ref) + branch = @project.repository.find_branch(@ref) + + # The files view would render a ref other than the default branch + # This redirect can be removed once the view is fixed + redirect_to(project_tree_path(@project, branch.target), alert: _("The default branch of this project clashes with another ref")) + return + end + respond_to do |format| format.html do @notification_setting = current_user.notification_settings_for(@project) if current_user @project = @project.present(current_user: current_user) - render_landing_page end diff --git a/app/finders/environments/environment_names_finder.rb b/app/finders/environments/environment_names_finder.rb index d4928f0fc8..ffb689f45e 100644 --- a/app/finders/environments/environment_names_finder.rb +++ b/app/finders/environments/environment_names_finder.rb @@ -32,18 +32,9 @@ module Environments end def namespace_environments - # We assume reporter access is needed for the :read_environment permission - # here. This expection is also present in - # IssuableFinder::Params#min_access_level, which is used for filtering out - # merge requests that don't have the right permissions. - # - # We use this approach so we don't need to load every project into memory - # just to verify if we can see their environments. Doing so would not be - # efficient, and possibly mess up pagination if certain projects are not - # meant to be visible. projects = project_or_group .all_projects - .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER) + .filter_by_feature_visibility(:environments, current_user) Environment.for_project(projects) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index c542ffbce7..ea16bc0102 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -30,6 +30,7 @@ class NotesFinder notes = init_collection notes = since_fetch_at(notes) notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter? + notes = redact_internal(notes) sort(notes) end @@ -181,6 +182,13 @@ class NotesFinder notes.order_by(sort) end + + def redact_internal(notes) + subject = @project || target + return notes if Ability.allowed?(@current_user, :reporter_access, subject) + + notes.not_internal + end end NotesFinder.prepend_mod_with('NotesFinder') diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 49418cda3a..a787eb1d5f 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -41,7 +41,7 @@ class WebHook < ApplicationRecord after_initialize :initialize_url_variables before_validation :reset_token - before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) } + before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) }, on: :update before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches? validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex? validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard? @@ -189,7 +189,7 @@ class WebHook < ApplicationRecord # See app/validators/json_schemas/web_hooks_url_variables.json VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze - def interpolated_url + def interpolated_url(url = self.url, url_variables = self.url_variables) return url unless url.include?('{') vars = url_variables @@ -215,7 +215,19 @@ class WebHook < ApplicationRecord end def reset_url_variables - self.url_variables = {} if url_changed? && !encrypted_url_variables_changed? + interpolated_url_was = interpolated_url(decrypt_url_was, url_variables_were) + + return if url_variables_were.empty? || interpolated_url_was == interpolated_url + + self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any? + end + + def decrypt_url_was + self.class.decrypt_url(encrypted_url_was, iv: Base64.decode64(encrypted_url_iv_was)) + end + + def url_variables_were + self.class.decrypt_url_variables(encrypted_url_variables_was, iv: encrypted_url_variables_iv_was) end def next_failure_count diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 11f4a3f3b6..d7bb9fa18a 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -36,7 +36,8 @@ class ProjectFeature < ApplicationRecord merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER, container_registry: Gitlab::Access::REPORTER, - package_registry: Gitlab::Access::REPORTER + package_registry: Gitlab::Access::REPORTER, + environments: Gitlab::Access::REPORTER }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b85a57f81c..cd05cc1763 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -404,7 +404,6 @@ class ProjectPolicy < BasePolicy end rule { can?(:metrics_dashboard) }.policy do - enable :read_prometheus enable :read_deployment end diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb index 711978dc3f..66af1c087e 100644 --- a/app/services/merge_requests/push_options_handler_service.rb +++ b/app/services/merge_requests/push_options_handler_service.rb @@ -54,7 +54,15 @@ module MergeRequests end def validate_service - errors << 'User is required' if current_user.nil? + if current_user.nil? + errors << 'User is required' + return + end + + unless current_user&.can?(:read_code, target_project) + errors << 'User access was denied' + return + end unless target_project.merge_requests_enabled? errors << "Merge requests are not enabled for project #{target_project.full_path}" diff --git a/app/views/explore/projects/page_out_of_bounds.html.haml b/app/views/explore/projects/page_out_of_bounds.html.haml index ef5ee2c679..e13768a3cc 100644 --- a/app/views/explore/projects/page_out_of_bounds.html.haml +++ b/app/views/explore/projects/page_out_of_bounds.html.haml @@ -18,5 +18,5 @@ %h5= _("Maximum page reached") %p= _("Sorry, you have exceeded the maximum browsable page number. Please use the API to explore further.") - = render Pajamas::ButtonComponent.new(href: request.params.merge(page: @max_page_number)) do + = render Pajamas::ButtonComponent.new(href: safe_params.merge(page: @max_page_number)) do = _("Back to page %{number}") % { number: @max_page_number } diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index fd80735024..543cbc4e4e 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -2,7 +2,7 @@ .tree-ref-container.gl-display-flex.mb-2.mb-md-0 .tree-ref-holder - #js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } } + #js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } } #js-repo-breadcrumb{ data: breadcrumb_data_attributes } diff --git a/db/post_migrate/20230208131808_nullify_last_error_from_project_mirror_data.rb b/db/post_migrate/20230208131808_nullify_last_error_from_project_mirror_data.rb new file mode 100644 index 0000000000..73e6f25749 --- /dev/null +++ b/db/post_migrate/20230208131808_nullify_last_error_from_project_mirror_data.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class NullifyLastErrorFromProjectMirrorData < Gitlab::Database::Migration[2.1] + MIGRATION = 'NullifyLastErrorFromProjectMirrorData' + INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 1_000 + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :project_mirror_data, + :id, + job_interval: INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :project_mirror_data, :id, []) + end +end diff --git a/db/schema_migrations/20230208131808 b/db/schema_migrations/20230208131808 new file mode 100644 index 0000000000..24c5b21f6a --- /dev/null +++ b/db/schema_migrations/20230208131808 @@ -0,0 +1 @@ +784f8f189eee7b5cf3136f0a859874a1d170d2b148f4c260f968b144816f1322 \ No newline at end of file diff --git a/glfm_specification/output_example_snapshots/snapshot_spec.html b/glfm_specification/output_example_snapshots/snapshot_spec.html index 415ad1d0b1..73e1cd0a23 100644 --- a/glfm_specification/output_example_snapshots/snapshot_spec.html +++ b/glfm_specification/output_example_snapshots/snapshot_spec.html @@ -7028,7 +7028,7 @@ references and their corresponding code points.
<p> & © Æ Ď
+<p> & © Æ Ď
¾ ℋ ⅆ
∲ ≧̸</p>
@@ -7344,11 +7344,11 @@ stripped in this way:
@@ -7356,12 +7356,12 @@ stripped in this way:
-` `
+` `
` `
-<p><code> </code>
+<p><code> </code>
<code> </code></p>
@@ -7832,11 +7832,11 @@ not part of a [left-flanking delimiter run]:
@@ -9790,7 +9790,7 @@ Other [Unicode whitespace] like non-breaking space doesn't work.
-[link](/url "title")
+[link](/url "title")
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 70535496b1..6f8d34ea38 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -203,6 +203,10 @@ module API
render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400)
end
+ unless can?(current_user, :read_code, target_project)
+ forbidden!("You don't have access to this fork's parent project")
+ end
+
cache_key = compare_cache_key(current_user, user_project, target_project, declared_params)
cache_action(cache_key, expires_in: 1.minute) do
diff --git a/lib/banzai/filter/inline_observability_filter.rb b/lib/banzai/filter/inline_observability_filter.rb
index 334c04f2b5..883965ccae 100644
--- a/lib/banzai/filter/inline_observability_filter.rb
+++ b/lib/banzai/filter/inline_observability_filter.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+require 'uri'
module Banzai
module Filter
@@ -15,7 +16,8 @@ module Banzai
doc.document.create_element(
'div',
class: 'js-render-observability',
- 'data-frame-url': url
+ 'data-frame-url': url,
+ 'data-observability-url': Gitlab::Observability.observability_url
)
end
@@ -28,8 +30,15 @@ module Banzai
# obtained from the target link
def element_to_embed(node)
url = node['href']
+ uri = URI.parse(url)
+ observability_uri = URI.parse(Gitlab::Observability.observability_url)
- create_element(url)
+ if uri.scheme == observability_uri.scheme &&
+ uri.port == observability_uri.port &&
+ uri.host.casecmp?(observability_uri.host) &&
+ uri.path.downcase.exclude?("auth/start")
+ create_element(url)
+ end
end
private
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index f22996df0a..23f93ad131 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -5,7 +5,8 @@
# Can be extended for different types of repository object, e.g. Project or Snippet
module ExtractsRef
InvalidPathError = Class.new(StandardError)
-
+ BRANCH_REF_TYPE = 'heads'
+ TAG_REF_TYPE = 'tags'
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
#
@@ -91,7 +92,7 @@ module ExtractsRef
def ref_type
return unless params[:ref_type].present?
- params[:ref_type] == 'tags' ? 'tags' : 'heads'
+ params[:ref_type] == TAG_REF_TYPE ? TAG_REF_TYPE : BRANCH_REF_TYPE
end
private
@@ -154,4 +155,13 @@ module ExtractsRef
def repository_container
raise NotImplementedError
end
+
+ def ambiguous_ref?(project, ref)
+ return true if project.repository.ambiguous_ref?(ref)
+
+ return false unless ref&.starts_with?('refs/')
+
+ unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '')
+ project.repository.commit(unprefixed_ref).present?
+ end
end
diff --git a/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data.rb b/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data.rb
new file mode 100644
index 0000000000..6ea5c17353
--- /dev/null
+++ b/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Nullifies last_error value from project_mirror_data table as they
+ # potentially included sensitive data.
+ # https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/3041
+ class NullifyLastErrorFromProjectMirrorData < BatchedMigrationJob
+ feature_category :source_code_management
+ operation_name :update_all
+
+ def perform
+ each_sub_batch { |rel| rel.update_all(last_error: nil) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/unicode.rb b/lib/gitlab/unicode.rb
index b49c5647da..f291ea1b4e 100644
--- a/lib/gitlab/unicode.rb
+++ b/lib/gitlab/unicode.rb
@@ -9,6 +9,12 @@ module Gitlab
# https://idiosyncratic-ruby.com/41-proper-unicoding.html
BIDI_REGEXP = /\p{Bidi Control}/.freeze
+ # Regular expression for identifying space characters
+ #
+ # In web browsers space characters can be confused with simple
+ # spaces which may be misleading
+ SPACE_REGEXP = /\p{Space_Separator}/.freeze
+
class << self
# Warning message used to highlight bidi characters in the GUI
def bidi_warning
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index e3bf11b00b..da078cc7b9 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -2,15 +2,37 @@
module Gitlab
class UrlSanitizer
+ include Gitlab::Utils::StrongMemoize
+
ALLOWED_SCHEMES = %w[http https ssh git].freeze
ALLOWED_WEB_SCHEMES = %w[http https].freeze
+ SCHEMIFIED_SCHEME = 'glschemelessuri'
+ SCHEMIFY_PLACEHOLDER = "#{SCHEMIFIED_SCHEME}://"
+ # URI::DEFAULT_PARSER.make_regexp will only match URLs with schemes or
+ # relative URLs. This section will match schemeless URIs with userinfo
+ # e.g. user:pass@gitlab.com but will not match scp-style URIs e.g.
+ # user@server:path/to/file)
+ #
+ # The userinfo part is very loose compared to URI's implementation so we
+ # also match non-escaped userinfo e.g foo:b?r@gitlab.com which should be
+ # encoded as foo:b%3Fr@gitlab.com
+ URI_REGEXP = %r{
+ (?:
+ #{URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)}
+ |
+ (?:(?:(?!@)[%#{URI::REGEXP::PATTERN::UNRESERVED}#{URI::REGEXP::PATTERN::RESERVED}])+(?:@))
+ (?# negative lookahead ensures this isn't an SCP-style URL: [host]:[rel_path|abs_path] server:path/to/file)
+ (?!#{URI::REGEXP::PATTERN::HOST}:(?:#{URI::REGEXP::PATTERN::REL_PATH}|#{URI::REGEXP::PATTERN::ABS_PATH}))
+ #{URI::REGEXP::PATTERN::HOSTPORT}
+ )
+ }x.freeze
def self.sanitize(content)
- regexp = URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)
-
- content.gsub(regexp) { |url| new(url).masked_url }
- rescue Addressable::URI::InvalidURIError
- content.gsub(regexp, '')
+ content.gsub(URI_REGEXP) do |url|
+ new(url).masked_url
+ rescue Addressable::URI::InvalidURIError
+ ''
+ end
end
def self.valid?(url, allowed_schemes: ALLOWED_SCHEMES)
@@ -37,17 +59,6 @@ module Gitlab
@url = parse_url(url)
end
- def sanitized_url
- @sanitized_url ||= safe_url.to_s
- end
-
- def masked_url
- url = @url.dup
- url.password = "*****" if url.password.present?
- url.user = "*****" if url.user.present?
- url.to_s
- end
-
def credentials
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
end
@@ -56,15 +67,37 @@ module Gitlab
credentials[:user]
end
- def full_url
- @full_url ||= generate_full_url.to_s
+ def sanitized_url
+ safe_url = @url.dup
+ safe_url.password = nil
+ safe_url.user = nil
+ reverse_schemify(safe_url.to_s)
end
+ strong_memoize_attr :sanitized_url
+
+ def masked_url
+ url = @url.dup
+ url.password = "*****" if url.password.present?
+ url.user = "*****" if url.user.present?
+ reverse_schemify(url.to_s)
+ end
+ strong_memoize_attr :masked_url
+
+ def full_url
+ return reverse_schemify(@url.to_s) unless valid_credentials?
+
+ url = @url.dup
+ url.password = encode_percent(credentials[:password]) if credentials[:password].present?
+ url.user = encode_percent(credentials[:user]) if credentials[:user].present?
+ reverse_schemify(url.to_s)
+ end
+ strong_memoize_attr :full_url
private
def parse_url(url)
- url = url.to_s.strip
- match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)})
+ url = schemify(url.to_s.strip)
+ match = url.match(%r{\A(?:(?:#{SCHEMIFIED_SCHEME}|git|ssh|http(?:s?)):)?//(?:(.+)(?:@))?(.+)}o)
raw_credentials = match[1] if match
if raw_credentials.present?
@@ -83,24 +116,19 @@ module Gitlab
url
end
- def generate_full_url
- return @url unless valid_credentials?
-
- @url.dup.tap do |generated|
- generated.password = encode_percent(credentials[:password]) if credentials[:password].present?
- generated.user = encode_percent(credentials[:user]) if credentials[:user].present?
- end
+ def schemify(url)
+ # Prepend the placeholder scheme unless the URL has a scheme or is relative
+ url.prepend(SCHEMIFY_PLACEHOLDER) unless url.starts_with?(%r{(?:#{URI::REGEXP::PATTERN::SCHEME}:)?//}o)
+ url
end
- def safe_url
- safe_url = @url.dup
- safe_url.password = nil
- safe_url.user = nil
- safe_url
+ def reverse_schemify(url)
+ url.slice!(SCHEMIFY_PLACEHOLDER) if url.starts_with?(SCHEMIFY_PLACEHOLDER)
+ url
end
def valid_credentials?
- credentials && credentials.is_a?(Hash) && credentials.any?
+ credentials.is_a?(Hash) && credentials.values.any?
end
def encode_percent(string)
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 436739bed1..a7e95a96b8 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -25,7 +25,10 @@ module Rouge
yield %()
line.each do |token, value|
- yield highlight_unicode_control_characters(span(token, value.chomp! || value))
+ value = value.chomp! || value
+ value = replace_space_characters(value)
+
+ yield highlight_unicode_control_characters(span(token, value))
end
yield ellipsis if @ellipsis_indexes.include?(@line_number - 1) && @ellipsis_svg.present?
@@ -42,6 +45,10 @@ module Rouge
%(#{@ellipsis_svg})
end
+ def replace_space_characters(text)
+ text.gsub(Gitlab::Unicode::SPACE_REGEXP, ' ')
+ end
+
def highlight_unicode_control_characters(text)
text.gsub(Gitlab::Unicode::BIDI_REGEXP) do |char|
%(#{char})
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ae06cb3ae7..f8626094af 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -42005,6 +42005,9 @@ msgstr ""
msgid "The default branch for this project has been changed. Please update your bookmarks."
msgstr ""
+msgid "The default branch of this project clashes with another ref"
+msgstr ""
+
msgid "The dependency list details information about the components used within your project."
msgstr ""
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 4101bd7f65..4e68ffdda2 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -59,6 +59,7 @@ RSpec.describe Admin::HooksController do
enable_ssl_verification: false,
url_variables: [
{ key: 'token', value: 'some secret value' },
+ { key: 'baz', value: 'qux' },
{ key: 'foo', value: nil }
]
}
@@ -71,7 +72,7 @@ RSpec.describe Admin::HooksController do
expect(flash[:notice]).to include('was updated')
expect(hook).to have_attributes(hook_params.except(:url_variables))
expect(hook).to have_attributes(
- url_variables: { 'token' => 'some secret value', 'baz' => 'woo' }
+ url_variables: { 'token' => 'some secret value', 'baz' => 'qux' }
)
end
end
diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb
index 334c156e1a..b8a4b94aa6 100644
--- a/spec/controllers/concerns/confirm_email_warning_spec.rb
+++ b/spec/controllers/concerns/confirm_email_warning_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ConfirmEmailWarning do
+RSpec.describe ConfirmEmailWarning, feature_category: :system_access do
before do
stub_feature_flags(soft_email_confirmation: true)
end
@@ -82,6 +82,38 @@ RSpec.describe ConfirmEmailWarning do
it { is_expected.to set_confirm_warning_for(user.email) }
end
end
+
+ context 'when user is being impersonated' do
+ let(:impersonator) { create(:admin) }
+
+ before do
+ allow(controller).to receive(:session).and_return({ impersonator_id: impersonator.id })
+
+ get :index
+ end
+
+ it { is_expected.to set_confirm_warning_for(user.email) }
+
+ context 'when impersonated user email has html in their email' do
+ let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: "malicious@test.com
-
---B_3664825007_1519466360--
-
-
---B_3664825007_384940722
-Content-type: image/png; name="gitlab_logo.png";
- x-mac-creator="4F50494D";
- x-mac-type="504E4766"
-Content-disposition: attachment;
- filename="gitlab_logo.png"
-Content-transfer-encoding: base64
-
-
-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
---B_3664825007_384940722--
-
---B_3664825007_1904734766
-Content-type: application/pkcs7-signature; name="smime.p7s"
-Content-transfer-encoding: base64
-Content-disposition: attachment;
- filename="smime.p7s"
-
-MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
-BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
-REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
-bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
-MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
-dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
-WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
-BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
-JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
-z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
-v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
-ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
-faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
-BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
-KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
-cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
-QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
-cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
-tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
-BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
-gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
-Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
-TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
-Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
-AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s
-b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G
-A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1
-R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ
-OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/
-0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m
-D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2
-NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940
-tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl
-gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5
-4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM
-tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px
-wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA
-MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ
-MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg
-VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw
-HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO
-MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
-bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
-vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
-q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
-Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
-Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
-lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
-JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
-BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
-HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
-Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
-dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
-VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
-AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
-MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
-BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
-WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
-WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
-cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
-BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
-BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
-eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
-R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
-hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
-7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
-DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
-ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
-K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
-lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
-7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
-BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
-DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
-ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
-aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
-BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
-hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
-AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
-DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
-sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
-8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
-YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
-
---B_3664825007_1904734766--
+User-Agent: Microsoft-MacOutlook/10.22.0.200209
+Date: Mon, 17 Feb 2020 22:56:47 +0100
+Subject: Re: htmltest | test issue (#1)
+From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)"
+
+To: Administrator / htmltest
+
+Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com>
+Thread-Topic: htmltest | test issue (#1)
+References:
+
+
+In-Reply-To:
+Content-type: multipart/signed;
+ protocol="application/pkcs7-signature";
+ micalg=sha256;
+ boundary="B_3664825007_1904734766"
+MIME-Version: 1.0
+
+--B_3664825007_1904734766
+Content-type: multipart/mixed;
+ boundary="B_3664825007_384940722"
+
+
+--B_3664825007_384940722
+Content-type: multipart/alternative;
+ boundary="B_3664825007_1519466360"
+
+
+--B_3664825007_1519466360
+Content-type: text/plain;
+ charset="UTF-8"
+Content-transfer-encoding: quoted-printable
+
+Me too, with an attachment
+
+=20
+
+From: Administrator
+Reply to: Administrator / htmltest
+Date: Monday, 17 February 2020 at 22:55
+To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)"
+Subject: Re: htmltest | test issue (#1)
+
+=20
+
+Administrator commented:=20
+
+I pity the foo !!!
+
+=E2=80=94=20
+Reply to this email directly or view it on GitLab.=20
+You're receiving this email because of your account on 169.254.169.254. If =
+you'd like to receive fewer emails, you can unsubscribe from this thread or =
+adjust your notification settings.=20
+
+
+--B_3664825007_1519466360
+Content-type: text/html;
+ charset="UTF-8"
+Content-transfer-encoding: quoted-printable
+
+GitLab Me too, with an attachment
From: Administrator <dlouzan.dummy@gma=
+il.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c0=
+34670b1623e617e15a3df64223d363@gmail.com>
Date: Monday, 17 Febr=
+uary 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT =
+STG XS)" <diego.louzan.ext@siemens.com>
Subject: Re: ht=
+mltest | test issue (#1)
=
+
Administrator commented:
<=
+div>I pity the foo !!!
=E2=80=94
Reply to this =
+email directly or view it on GitLab.
You're receiving this email because of you=
+r account on 169.254.169.254. If you'd like to receive fewer emails, you can=
+ unsubscribe from this thread or adjust your notific=
+ation settings.
+
+--B_3664825007_1519466360--
+
+
+--B_3664825007_384940722
+Content-type: image/png; name="gitlab_logo.png";
+ x-mac-creator="4F50494D";
+ x-mac-type="504E4766"
+Content-disposition: attachment;
+ filename="gitlab_logo.png"
+Content-transfer-encoding: base64
+
+
+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
+--B_3664825007_384940722--
+
+--B_3664825007_1904734766
+Content-type: application/pkcs7-signature; name="smime.p7s"
+Content-transfer-encoding: base64
+Content-disposition: attachment;
+ filename="smime.p7s"
+
+MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
+BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
+REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
+bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
+MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
+dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
+WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
+BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
+JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
+z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
+v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
+ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
+faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
+BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
+KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
+cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
+QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
+cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
+tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
+BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
+gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
+Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
+TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
+Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s
+b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G
+A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1
+R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ
+OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/
+0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m
+D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2
+NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940
+tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl
+gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5
+4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM
+tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px
+wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA
+MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ
+MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg
+VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw
+HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO
+MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
+bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
+vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
+q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
+Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
+Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
+lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
+JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
+BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
+HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
+Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
+dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
+VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
+AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
+MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
+BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
+WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
+WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
+cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
+BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
+BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
+eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
+R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
+hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
+7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
+DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
+ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
+K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
+lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
+7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
+BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
+DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
+ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
+aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
+BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
+AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
+DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
+sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
+8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
+YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
+
+--B_3664825007_1904734766--
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
index c87d11742d..03a0cb2fcc 100644
--- a/spec/frontend/behaviors/markdown/render_observability_spec.js
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -16,7 +16,7 @@ describe('Observability iframe renderer', () => {
});
it('renders an observability iframe', () => {
- document.body.innerHTML = ``;
+ document.body.innerHTML = ``;
expect(findObservabilityIframes()).toHaveLength(0);
@@ -26,7 +26,7 @@ describe('Observability iframe renderer', () => {
});
it('renders iframe with dark param when GL has dark theme', () => {
- document.body.innerHTML = ``;
+ document.body.innerHTML = ``;
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
expect(findObservabilityIframes('dark')).toHaveLength(0);
@@ -35,4 +35,12 @@ describe('Observability iframe renderer', () => {
expect(findObservabilityIframes('dark')).toHaveLength(1);
});
+
+ it('does not render if url is different from observability url', () => {
+ document.body.innerHTML = ``;
+
+ renderEmbeddedObservability();
+
+ expect(findObservabilityIframes()).toHaveLength(0);
+ });
});
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
index 4d0250fffb..220dbf1739 100644
--- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -1,5 +1,6 @@
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'spec/test_constants';
import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
const projectRootPath = 'root/Project1';
@@ -16,14 +17,38 @@ describe('generateRefDestinationPath', () => {
${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
- `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
setWindowLocation(currentPath);
- expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(
+ `${TEST_HOST}/${result}`,
+ );
+ });
+
+ describe('when using symbolic ref names', () => {
+ it.each`
+ currentPath | nextRef | result
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'someHash'} | ${`${projectRootPath}/-/blob/someHash/dir1/dir2/test.js#L123`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/refs/heads/branchNameContainsPrefix'} | ${`${projectRootPath}/-/tree/refs/heads/branchNameContainsPrefix/dir1/dir2/test.js?ref_type=heads#L123`}
+ `(
+ 'generates the correct destination path for $currentPath with ref type when it can be extracted',
+ ({ currentPath, result, nextRef }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, nextRef)).toBe(
+ `${TEST_HOST}/${result}`,
+ );
+ },
+ );
});
it('encodes the selected ref', () => {
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
- expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
+ `${TEST_HOST}/${result}`,
+ );
});
});
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 98a1f77b41..83d0a86eaa 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe HooksHelper do
it 'returns proper data' do
expect(subject).to match(
url: project_hook.url,
- url_variables: Gitlab::Json.dump([{ key: 'abc' }])
+ url_variables: Gitlab::Json.dump([{ key: 'abc' }, { key: 'def' }])
)
end
end
diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
index fb1ba46e76..69a9dc96c2 100644
--- a/spec/lib/banzai/filter/inline_observability_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
@@ -34,6 +34,58 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do
end
end
+ context 'when the document contains an embeddable observability link with redirect' do
+ let(:url) { 'https://observe.gitlab.com@example.com/12345' }
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq(input)
+ end
+
+ it 'does not append an observability charts placeholder' do
+ node = doc.at_css('.js-render-observability')
+
+ expect(node).not_to be_present
+ end
+ end
+
+ context 'when the document contains an embeddable observability link with different port' do
+ let(:url) { 'https://observe.gitlab.com:3000/12345' }
+ let(:observe_url) { 'https://observe.gitlab.com:3001' }
+
+ before do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url)
+ end
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq(input)
+ end
+
+ it 'does not append an observability charts placeholder' do
+ node = doc.at_css('.js-render-observability')
+
+ expect(node).not_to be_present
+ end
+ end
+
+ context 'when the document contains an embeddable observability link with auth/start' do
+ let(:url) { 'https://observe.gitlab.com/auth/start' }
+ let(:observe_url) { 'https://observe.gitlab.com' }
+
+ before do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url)
+ end
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq(input)
+ end
+
+ it 'does not append an observability charts placeholder' do
+ node = doc.at_css('.js-render-observability')
+
+ expect(node).not_to be_present
+ end
+ end
+
context 'when feature flag is disabled' do
let(:url) { 'https://observe.gitlab.com/12345' }
diff --git a/spec/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data_spec.rb b/spec/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data_spec.rb
new file mode 100644
index 0000000000..62f908ed79
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::NullifyLastErrorFromProjectMirrorData, feature_category: :source_code_management do # rubocop:disable Layout/LineLength
+ it 'nullifies last_error column on all rows' do
+ namespaces = table(:namespaces)
+ projects = table(:projects)
+ project_import_states = table(:project_mirror_data)
+
+ group = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
+
+ project_namespace_1 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
+ project_namespace_2 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
+ project_namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
+
+ project_1 = projects.create!(
+ namespace_id: group.id,
+ project_namespace_id: project_namespace_1.id,
+ name: 'test1'
+ )
+ project_2 = projects.create!(
+ namespace_id: group.id,
+ project_namespace_id: project_namespace_2.id,
+ name: 'test2'
+ )
+ project_3 = projects.create!(
+ namespace_id: group.id,
+ project_namespace_id: project_namespace_3.id,
+ name: 'test3'
+ )
+
+ project_import_state_1 = project_import_states.create!(
+ project_id: project_1.id,
+ status: 0,
+ last_update_started_at: 1.hour.ago,
+ last_update_scheduled_at: 1.hour.ago,
+ last_update_at: 1.hour.ago,
+ last_successful_update_at: 2.days.ago,
+ last_error: '13:fetch remote: "fatal: unable to look up user:pass@gitlab.com (port 9418) (nodename nor servname provided, or not known)\n": exit status 128.', # rubocop:disable Layout/LineLength
+ correlation_id_value: SecureRandom.uuid,
+ jid: SecureRandom.uuid
+ )
+
+ project_import_states.create!(
+ project_id: project_2.id,
+ status: 1,
+ last_update_started_at: 1.hour.ago,
+ last_update_scheduled_at: 1.hour.ago,
+ last_update_at: 1.hour.ago,
+ last_successful_update_at: nil,
+ next_execution_timestamp: 1.day.from_now,
+ last_error: '',
+ correlation_id_value: SecureRandom.uuid,
+ jid: SecureRandom.uuid
+ )
+
+ project_import_state_3 = project_import_states.create!(
+ project_id: project_3.id,
+ status: 2,
+ last_update_started_at: 1.hour.ago,
+ last_update_scheduled_at: 1.hour.ago,
+ last_update_at: 1.hour.ago,
+ last_successful_update_at: 1.hour.ago,
+ next_execution_timestamp: 1.day.from_now,
+ last_error: nil,
+ correlation_id_value: SecureRandom.uuid,
+ jid: SecureRandom.uuid
+ )
+
+ migration = described_class.new(
+ start_id: project_import_state_1.id,
+ end_id: project_import_state_3.id,
+ batch_table: :project_mirror_data,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+
+ w_last_error_count = -> { project_import_states.where.not(last_error: nil).count } # rubocop:disable CodeReuse/ActiveRecord
+ expect { migration.perform }.to change(&w_last_error_count).from(2).to(0)
+ end
+end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 0ffbf5f81e..c02cbef832 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -10,29 +10,36 @@ RSpec.describe Gitlab::UrlSanitizer do
# We want to try with multi-line content because is how error messages are formatted
described_class.sanitize(%Q{
remote: Not Found
- fatal: repository '#{url}' not found
+ fatal: repository `#{url}` not found
})
end
where(:input, :output) do
- 'http://user:pass@test.com/root/repoC.git/' | 'http://*****:*****@test.com/root/repoC.git/'
- 'https://user:pass@test.com/root/repoA.git/' | 'https://*****:*****@test.com/root/repoA.git/'
- 'ssh://user@host.test/path/to/repo.git' | 'ssh://*****@host.test/path/to/repo.git'
-
- # git protocol does not support authentication but clean any details anyway
- 'git://user:pass@host.test/path/to/repo.git' | 'git://*****:*****@host.test/path/to/repo.git'
- 'git://host.test/path/to/repo.git' | 'git://host.test/path/to/repo.git'
+ # http(s), ssh, git, relative, and schemeless URLs should all be masked correctly
+ urls = ['http://', 'https://', 'ssh://', 'git://', '//', ''].flat_map do |protocol|
+ [
+ ["#{protocol}test.com", "#{protocol}test.com"],
+ ["#{protocol}test.com/", "#{protocol}test.com/"],
+ ["#{protocol}test.com/path/to/repo.git", "#{protocol}test.com/path/to/repo.git"],
+ ["#{protocol}user@test.com", "#{protocol}*****@test.com"],
+ ["#{protocol}user:pass@test.com", "#{protocol}*****:*****@test.com"],
+ ["#{protocol}user:@test.com", "#{protocol}*****@test.com"],
+ ["#{protocol}:pass@test.com", "#{protocol}:*****@test.com"]
+ ]
+ end
# SCP-style URLs are left unmodified
- 'user@server:project.git' | 'user@server:project.git'
- 'user:pass@server:project.git' | 'user:pass@server:project.git'
+ urls << ['user@server:project.git', 'user@server:project.git']
+ urls << ['user:@server:project.git', 'user:@server:project.git']
+ urls << [':pass@server:project.git', ':pass@server:project.git']
+ urls << ['user:pass@server:project.git', 'user:pass@server:project.git']
# return an empty string for invalid URLs
- 'ssh://' | ''
+ urls << ['ssh://', '']
end
with_them do
- it { expect(sanitize_url(input)).to include("repository '#{output}' not found") }
+ it { expect(sanitize_url(input)).to include("repository `#{output}` not found") }
end
end
diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb
index 79bfdb262c..6fc1b395fc 100644
--- a/spec/lib/rouge/formatters/html_gitlab_spec.rb
+++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Rouge::Formatters::HTMLGitlab do
+RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_management do
describe '#format' do
subject { described_class.format(tokens, **options) }
@@ -67,5 +67,24 @@ RSpec.describe Rouge::Formatters::HTMLGitlab do
is_expected.to include(%{}).exactly(4).times
end
end
+
+ context 'when space characters and zero-width spaces are used' do
+ let(:lang) { 'ruby' }
+ let(:tokens) { lexer.lex(code, continue: false) }
+
+ let(:code) do
+ <<~JS
+ def\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000hello
+ JS
+ end
+
+ it 'replaces the space characters with spaces' do
+ is_expected.to eq(
+ "" \
+ "def hello" \
+ ""
+ )
+ end
+ end
end
end
diff --git a/spec/migrations/nullify_last_error_from_project_mirror_data_spec.rb b/spec/migrations/nullify_last_error_from_project_mirror_data_spec.rb
new file mode 100644
index 0000000000..6c5679b674
--- /dev/null
+++ b/spec/migrations/nullify_last_error_from_project_mirror_data_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe NullifyLastErrorFromProjectMirrorData, feature_category: :source_code_management do
+ let(:migration) { described_class::MIGRATION }
+
+ before do
+ migrate!
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of projects' do
+ expect(migration).to(
+ have_scheduled_batched_migration(
+ table_name: :project_mirror_data,
+ column_name: :id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ )
+ end
+ end
+
+ describe '#down' do
+ before do
+ schema_migrate_down!
+ end
+
+ it 'deletes all batched migration records' do
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index d48f6f7f3e..26795c0ea7 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -302,7 +302,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
it { expect(result[:issue].gitlab_commit_path).to eq(nil) }
end
- context 'when repo commit matches first relase version' do
+ context 'when repo commit matches first release version' do
let(:commit) { instance_double(Commit, id: commit_id) }
let(:repository) { instance_double(Repository, commit: commit) }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 75ff917c03..48d5b3b83c 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -242,6 +242,22 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(hook.url_variables).to eq({})
end
+ it 'resets url variables if url is changed and url variables are appended' do
+ hook.url = 'http://suspicious.example.com/{abc}/{foo}'
+ hook.url_variables = hook.url_variables.merge('foo' => 'bar')
+
+ expect(hook).not_to be_valid
+ expect(hook.url_variables).to eq({})
+ end
+
+ it 'resets url variables if url is changed and url variables are removed' do
+ hook.url = 'http://suspicious.example.com/{abc}'
+ hook.url_variables = hook.url_variables.except("def")
+
+ expect(hook).not_to be_valid
+ expect(hook.url_variables).to eq({})
+ end
+
it 'does not reset url variables if both url and url variables are changed' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
@@ -249,6 +265,18 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(hook).to be_valid
expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
end
+
+ context 'without url variables' do
+ subject(:hook) { build_stubbed(:project_hook, project: project, url: 'http://example.com') }
+
+ it 'does not reset url variables' do
+ hook.url = 'http://example.com/{one}/{two}'
+ hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
+
+ expect(hook).to be_valid
+ expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
+ end
+ end
end
it "only consider these branch filter strategies are valid" do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a98f091b9f..52d1e70d7a 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -697,6 +697,39 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
+ describe 'read_prometheus', feature_category: :metrics do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ let(:policy) { :read_prometheus }
+
+ where(:project_visibility, :role, :allowed) do
+ :public | :anonymous | false
+ :public | :guest | false
+ :public | :reporter | true
+ :internal | :anonymous | false
+ :internal | :guest | false
+ :internal | :reporter | true
+ :private | :anonymous | false
+ :private | :guest | false
+ :private | :reporter | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+ let(:project) { public_send("#{project_visibility}_project") }
+
+ if params[:allowed]
+ it { is_expected.to be_allowed(policy) }
+ else
+ it { is_expected.not_to be_allowed(policy) }
+ end
+ end
+ end
+
describe 'update_max_artifacts_size' do
context 'when no user' do
let(:current_user) { anonymous }
@@ -972,7 +1005,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { guest }
it { is_expected.to be_allowed(:metrics_dashboard) }
- it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
@@ -982,7 +1015,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { anonymous }
it { is_expected.to be_allowed(:metrics_dashboard) }
- it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_disallowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_disallowed(:create_metrics_user_starred_dashboard) }
@@ -1008,12 +1041,14 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
end
@@ -1036,7 +1071,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { guest }
it { is_expected.to be_allowed(:metrics_dashboard) }
- it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
@@ -1046,6 +1081,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
end
end
@@ -1068,12 +1104,14 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
end
@@ -1092,12 +1130,14 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
+ it { is_expected.to be_disallowed(:read_prometheus) }
end
end
end
@@ -2016,7 +2056,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
:public | ProjectFeature::ENABLED | :anonymous | true
:public | ProjectFeature::PRIVATE | :maintainer | true
:public | ProjectFeature::PRIVATE | :developer | true
- :public | ProjectFeature::PRIVATE | :guest | true
+ :public | ProjectFeature::PRIVATE | :guest | false
:public | ProjectFeature::PRIVATE | :anonymous | false
:public | ProjectFeature::DISABLED | :maintainer | false
:public | ProjectFeature::DISABLED | :developer | false
@@ -2028,7 +2068,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
:internal | ProjectFeature::ENABLED | :anonymous | false
:internal | ProjectFeature::PRIVATE | :maintainer | true
:internal | ProjectFeature::PRIVATE | :developer | true
- :internal | ProjectFeature::PRIVATE | :guest | true
+ :internal | ProjectFeature::PRIVATE | :guest | false
:internal | ProjectFeature::PRIVATE | :anonymous | false
:internal | ProjectFeature::DISABLED | :maintainer | false
:internal | ProjectFeature::DISABLED | :developer | false
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 555ba2bc97..be26fe2406 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -573,6 +573,22 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository compare' do
let(:current_user) { user }
+
+ context 'when user does not have read access to the parent project' do
+ let_it_be(:group) { create(:group) }
+ let(:forked_project) { fork_project(project, current_user, repository: true, namespace: group) }
+
+ before do
+ forked_project.add_guest(current_user)
+ end
+
+ it 'returns 403 error' do
+ get api(route, current_user), params: { from: 'improve/awesome', to: 'feature', from_project_id: forked_project.id }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq("403 Forbidden - You don't have access to this fork's parent project")
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 251bf6f0d9..03f3d56cdd 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -861,6 +861,21 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
end
end
+ describe 'when user does not have access to target project' do
+ let(:push_options) { { create: true, target: 'my-branch' } }
+ let(:changes) { default_branch_changes }
+
+ before do
+ allow(user1).to receive(:can?).with(:read_code, project).and_return(false)
+ end
+
+ it 'records an error', :sidekiq_inline do
+ service.execute
+
+ expect(service.errors).to eq(["User access was denied"])
+ end
+ end
+
describe 'when MRs are not enabled' do
let(:project) { create(:project, :public, :repository).tap { |pr| pr.add_developer(user1) } }
let(:push_options) { { create: true } }
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 4b925a058e..bc11895a12 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -130,8 +130,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
context 'there is userinfo' do
before do
project_hook.update!(
- url: 'http://{one}:{two}@example.com',
- url_variables: { 'one' => 'a', 'two' => 'b' }
+ url: 'http://{foo}:{bar}@example.com',
+ url_variables: { 'foo' => 'a', 'bar' => 'b' }
)
stub_full_request('http://example.com', method: :post)
end
diff --git a/spec/views/explore/projects/page_out_of_bounds.html.haml_spec.rb b/spec/views/explore/projects/page_out_of_bounds.html.haml_spec.rb
new file mode 100644
index 0000000000..1ace28be5b
--- /dev/null
+++ b/spec/views/explore/projects/page_out_of_bounds.html.haml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'explore/projects/page_out_of_bounds.html.haml', feature_category: :projects do
+ let(:page_limit) { 10 }
+ let(:unsafe_param) { 'hacked_using_unsafe_param!' }
+
+ before do
+ assign(:max_page_number, page_limit)
+
+ controller.params[:action] = 'index'
+ controller.params[:host] = unsafe_param
+ controller.params[:protocol] = unsafe_param
+ controller.params[:sort] = 'name_asc'
+ end
+
+ it 'removes unsafe params from the link' do
+ render
+
+ href = "/explore/projects?page=#{page_limit}&sort=name_asc"
+ button_text = format(_("Back to page %{number}"), number: page_limit)
+ expect(rendered).to have_link(button_text, href: href)
+ expect(rendered).not_to include(unsafe_param)
+ end
+end