New upstream version 11.10.5+dfsg
This commit is contained in:
parent
688aff3489
commit
8cb3f74f52
72 changed files with 1193 additions and 169 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -2,6 +2,24 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 11.10.5 (2019-05-30)
|
||||||
|
|
||||||
|
### Security (12 changes, 1 of them is from the community)
|
||||||
|
|
||||||
|
- Protect Gitlab::HTTP against DNS rebinding attack.
|
||||||
|
- Fix project visibility level validation. (Peter Marko)
|
||||||
|
- Update Knative version.
|
||||||
|
- Add DNS rebinding protection settings.
|
||||||
|
- Prevent XSS injection in note imports.
|
||||||
|
- Prevent invalid branch for merge request.
|
||||||
|
- Filter relative links in wiki for XSS.
|
||||||
|
- Fix confidential issue label disclosure on milestone view.
|
||||||
|
- Fix url redaction for issue links.
|
||||||
|
- Resolve: Milestones leaked via search API.
|
||||||
|
- Prevent bypass of restriction disabling web password sign in.
|
||||||
|
- Hide confidential issue title on unsubscribe for anonymous users.
|
||||||
|
|
||||||
|
|
||||||
## 11.10.4 (2019-05-01)
|
## 11.10.4 (2019-05-01)
|
||||||
|
|
||||||
### Fixed (12 changes)
|
### Fixed (12 changes)
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
11.10.4
|
11.10.5
|
||||||
|
|
|
@ -31,7 +31,7 @@ module MilestoneActions
|
||||||
format.html { redirect_to milestone_redirect_path }
|
format.html { redirect_to milestone_redirect_path }
|
||||||
format.json do
|
format.json do
|
||||||
render json: tabs_json("shared/milestones/_labels_tab", {
|
render json: tabs_json("shared/milestones/_labels_tab", {
|
||||||
labels: @milestone.labels # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
labels: @milestone.issue_labels_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,8 @@ class SessionsController < Devise::SessionsController
|
||||||
prepend_before_action :check_captcha, only: [:create]
|
prepend_before_action :check_captcha, only: [:create]
|
||||||
prepend_before_action :store_redirect_uri, only: [:new]
|
prepend_before_action :store_redirect_uri, only: [:new]
|
||||||
prepend_before_action :ldap_servers, only: [:new, :create]
|
prepend_before_action :ldap_servers, only: [:new, :create]
|
||||||
|
prepend_before_action :ensure_password_authentication_enabled!, if: :password_based_login?, only: [:create]
|
||||||
|
|
||||||
before_action :auto_sign_in_with_provider, only: [:new]
|
before_action :auto_sign_in_with_provider, only: [:new]
|
||||||
before_action :load_recaptcha
|
before_action :load_recaptcha
|
||||||
|
|
||||||
|
@ -126,6 +128,14 @@ class SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
def ensure_password_authentication_enabled!
|
||||||
|
render_403 unless Gitlab::CurrentSettings.password_authentication_enabled_for_web?
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_based_login?
|
||||||
|
user_params[:login].present? || user_params[:password].present?
|
||||||
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
|
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
|
||||||
end
|
end
|
||||||
|
|
|
@ -160,6 +160,7 @@ module ApplicationSettingsHelper
|
||||||
:akismet_api_key,
|
:akismet_api_key,
|
||||||
:akismet_enabled,
|
:akismet_enabled,
|
||||||
:allow_local_requests_from_hooks_and_services,
|
:allow_local_requests_from_hooks_and_services,
|
||||||
|
:dns_rebinding_protection_enabled,
|
||||||
:archive_builds_in_human_readable,
|
:archive_builds_in_human_readable,
|
||||||
:authorized_keys_enabled,
|
:authorized_keys_enabled,
|
||||||
:auto_devops_enabled,
|
:auto_devops_enabled,
|
||||||
|
|
|
@ -93,4 +93,8 @@ module NotificationsHelper
|
||||||
s_(event.to_s.humanize)
|
s_(event.to_s.humanize)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_unsubscribe_title?(noteable)
|
||||||
|
can?(current_user, "read_#{noteable.to_ability_name}".to_sym, noteable)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ module ApplicationSettingImplementation
|
||||||
after_sign_up_text: nil,
|
after_sign_up_text: nil,
|
||||||
akismet_enabled: false,
|
akismet_enabled: false,
|
||||||
allow_local_requests_from_hooks_and_services: false,
|
allow_local_requests_from_hooks_and_services: false,
|
||||||
|
dns_rebinding_protection_enabled: true,
|
||||||
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
|
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
|
||||||
container_registry_token_expire_delay: 5,
|
container_registry_token_expire_delay: 5,
|
||||||
default_artifacts_expire_in: '30 days',
|
default_artifacts_expire_in: '30 days',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module Clusters
|
module Clusters
|
||||||
module Applications
|
module Applications
|
||||||
class Knative < ApplicationRecord
|
class Knative < ApplicationRecord
|
||||||
VERSION = '0.3.0'.freeze
|
VERSION = '0.5.0'.freeze
|
||||||
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
|
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
|
||||||
METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze
|
METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze
|
||||||
FETCH_IP_ADDRESS_DELAY = 30.seconds
|
FETCH_IP_ADDRESS_DELAY = 30.seconds
|
||||||
|
|
|
@ -620,6 +620,8 @@ class MergeRequest < ApplicationRecord
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
[:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }
|
||||||
|
|
||||||
if opened?
|
if opened?
|
||||||
similar_mrs = target_project
|
similar_mrs = target_project
|
||||||
.merge_requests
|
.merge_requests
|
||||||
|
@ -640,6 +642,16 @@ class MergeRequest < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_branch_name(attr)
|
||||||
|
return unless changes_include?(attr)
|
||||||
|
|
||||||
|
branch = read_attribute(attr)
|
||||||
|
|
||||||
|
return unless branch
|
||||||
|
|
||||||
|
errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
|
||||||
|
end
|
||||||
|
|
||||||
def validate_target_project
|
def validate_target_project
|
||||||
return true if target_project.merge_requests_enabled?
|
return true if target_project.merge_requests_enabled?
|
||||||
|
|
||||||
|
|
|
@ -334,8 +334,8 @@ class Project < ApplicationRecord
|
||||||
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
|
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
|
||||||
validate :check_personal_projects_limit, on: :create
|
validate :check_personal_projects_limit, on: :create
|
||||||
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
|
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
|
||||||
validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
|
validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
|
||||||
validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
|
validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
|
||||||
validate :check_wiki_path_conflict
|
validate :check_wiki_path_conflict
|
||||||
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
|
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
|
||||||
validates :repository_storage,
|
validates :repository_storage,
|
||||||
|
@ -403,6 +403,7 @@ class Project < ApplicationRecord
|
||||||
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
|
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
|
||||||
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
|
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
|
||||||
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
|
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
|
||||||
|
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
|
||||||
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
|
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
|
||||||
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
|
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
|
||||||
|
|
||||||
|
@ -587,6 +588,17 @@ class Project < ApplicationRecord
|
||||||
def group_ids
|
def group_ids
|
||||||
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
|
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns ids of projects with milestones available for given user
|
||||||
|
#
|
||||||
|
# Used on queries to find milestones which user can see
|
||||||
|
# For example: Milestone.where(project_id: ids_with_milestone_available_for(user))
|
||||||
|
def ids_with_milestone_available_for(user)
|
||||||
|
with_issues_enabled = with_issues_available_for_user(user).select(:id)
|
||||||
|
with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
|
||||||
|
|
||||||
|
from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_pipelines
|
def all_pipelines
|
||||||
|
@ -878,6 +890,10 @@ class Project < ApplicationRecord
|
||||||
self.errors.add(:limit_reached, error % { limit: limit })
|
self.errors.add(:limit_reached, error % { limit: limit })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def should_validate_visibility_level?
|
||||||
|
new_record? || changes.has_key?(:visibility_level)
|
||||||
|
end
|
||||||
|
|
||||||
def visibility_level_allowed_by_group
|
def visibility_level_allowed_by_group
|
||||||
return if visibility_level_allowed_by_group?
|
return if visibility_level_allowed_by_group?
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,12 @@
|
||||||
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
|
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
|
||||||
Allow requests to the local network from hooks and services
|
Allow requests to the local network from hooks and services
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
.form-check
|
||||||
|
= f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
|
||||||
|
= f.label :dns_rebinding_protection_enabled, class: 'form-check-label' do
|
||||||
|
= _('Enforce DNS rebinding attack protection')
|
||||||
|
%span.form-text.text-muted
|
||||||
|
= _('Resolves IP addresses once and uses them to submit requests')
|
||||||
|
|
||||||
= f.submit 'Save changes', class: "btn btn-success"
|
= f.submit 'Save changes', class: "btn btn-success"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- noteable = @sent_notification.noteable
|
- noteable = @sent_notification.noteable
|
||||||
- noteable_type = @sent_notification.noteable_type.titleize.downcase
|
- noteable_type = @sent_notification.noteable_type.titleize.downcase
|
||||||
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
|
- noteable_text = show_unsubscribe_title?(noteable) ? %(#{noteable.title} (#{noteable.to_reference})) : %(#{noteable.to_reference})
|
||||||
- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
|
- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
|
||||||
|
|
||||||
%h3.page-title
|
%h3.page-title
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
|
# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
|
||||||
module HipChat
|
module HipChat
|
||||||
class Client
|
class Client
|
||||||
connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
|
connection_adapter ::Gitlab::HTTPConnectionAdapter
|
||||||
end
|
end
|
||||||
|
|
||||||
class Room
|
class Room
|
||||||
connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
|
connection_adapter ::Gitlab::HTTPConnectionAdapter
|
||||||
end
|
end
|
||||||
|
|
||||||
class User
|
class User
|
||||||
connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
|
connection_adapter ::Gitlab::HTTPConnectionAdapter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
49
config/initializers/http_hostname_override.rb
Normal file
49
config/initializers/http_hostname_override.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# This override allows passing `@hostname_override` to the SNI protocol,
|
||||||
|
# which is used to lookup the correct SSL certificate in the
|
||||||
|
# request handshake process.
|
||||||
|
#
|
||||||
|
# Given we've forced the HTTP request to be sent to the resolved
|
||||||
|
# IP address in a few scenarios (e.g.: `Gitlab::HTTP` through
|
||||||
|
# `Gitlab::UrlBlocker.validate!`), we need to provide the _original_
|
||||||
|
# hostname via SNI in order to have a clean connection setup.
|
||||||
|
#
|
||||||
|
# This is ultimately needed in order to avoid DNS rebinding attacks
|
||||||
|
# through HTTP requests.
|
||||||
|
#
|
||||||
|
class OpenSSL::SSL::SSLContext
|
||||||
|
attr_accessor :hostname_override
|
||||||
|
end
|
||||||
|
|
||||||
|
class OpenSSL::SSL::SSLSocket
|
||||||
|
module HostnameOverride
|
||||||
|
# rubocop: disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
def hostname=(hostname)
|
||||||
|
super(@context.hostname_override || hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_connection_check(hostname)
|
||||||
|
super(@context.hostname_override || hostname)
|
||||||
|
end
|
||||||
|
# rubocop: enable Gitlab/ModuleWithInstanceVariables
|
||||||
|
end
|
||||||
|
|
||||||
|
prepend HostnameOverride
|
||||||
|
end
|
||||||
|
|
||||||
|
class Net::HTTP
|
||||||
|
attr_accessor :hostname_override
|
||||||
|
SSL_IVNAMES << :@hostname_override
|
||||||
|
SSL_ATTRIBUTES << :hostname_override
|
||||||
|
|
||||||
|
module HostnameOverride
|
||||||
|
def addr_port
|
||||||
|
return super unless hostname_override
|
||||||
|
|
||||||
|
addr = hostname_override
|
||||||
|
default_port = use_ssl? ? Net::HTTP.https_default_port : Net::HTTP.http_default_port
|
||||||
|
default_port == port ? addr : "#{addr}:#{port}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
prepend HostnameOverride
|
||||||
|
end
|
|
@ -266,6 +266,6 @@
|
||||||
weight: 1
|
weight: 1
|
||||||
queries:
|
queries:
|
||||||
- id: system_metrics_knative_function_invocation_count
|
- id: system_metrics_knative_function_invocation_count
|
||||||
query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))'
|
query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])/3))'
|
||||||
label: invocations / minute
|
label: invocations / minute
|
||||||
unit: requests
|
unit: requests
|
||||||
|
|
20
db/migrate/20190408163745_prometheus_knative05_fix.rb
Normal file
20
db/migrate/20190408163745_prometheus_knative05_fix.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||||
|
# for more information on how to write migrations for GitLab.
|
||||||
|
|
||||||
|
class PrometheusKnative05Fix < ActiveRecord::Migration[5.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
require Rails.root.join('db/importers/common_metrics_importer.rb')
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def up
|
||||||
|
Importers::CommonMetricsImporter.new.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# no-op
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||||
|
# for more information on how to write migrations for GitLab.
|
||||||
|
|
||||||
|
class AddDnsRebindingProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column_with_default(:application_settings, :dns_rebinding_protection_enabled,
|
||||||
|
:boolean,
|
||||||
|
default: true,
|
||||||
|
allow_null: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column(:application_settings, :dns_rebinding_protection_enabled)
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20190326164045) do
|
ActiveRecord::Schema.define(version: 20190529142545) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -187,6 +187,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
|
||||||
t.string "encrypted_external_auth_client_key_iv"
|
t.string "encrypted_external_auth_client_key_iv"
|
||||||
t.string "encrypted_external_auth_client_key_pass"
|
t.string "encrypted_external_auth_client_key_pass"
|
||||||
t.string "encrypted_external_auth_client_key_pass_iv"
|
t.string "encrypted_external_auth_client_key_pass_iv"
|
||||||
|
t.boolean "dns_rebinding_protection_enabled", default: true, null: false
|
||||||
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
|
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ module Banzai
|
||||||
module Filter
|
module Filter
|
||||||
class WikiLinkFilter < HTML::Pipeline::Filter
|
class WikiLinkFilter < HTML::Pipeline::Filter
|
||||||
class Rewriter
|
class Rewriter
|
||||||
|
UNSAFE_SLUG_REGEXES = [/\Ajavascript:/i].freeze
|
||||||
|
|
||||||
def initialize(link_string, wiki:, slug:)
|
def initialize(link_string, wiki:, slug:)
|
||||||
@uri = Addressable::URI.parse(link_string)
|
@uri = Addressable::URI.parse(link_string)
|
||||||
@wiki_base_path = wiki && wiki.wiki_base_path
|
@wiki_base_path = wiki && wiki.wiki_base_path
|
||||||
|
@ -35,6 +37,8 @@ module Banzai
|
||||||
|
|
||||||
# Of the form `./link`, `../link`, or similar
|
# Of the form `./link`, `../link`, or similar
|
||||||
def apply_hierarchical_link_rules!
|
def apply_hierarchical_link_rules!
|
||||||
|
return if slug_considered_unsafe?
|
||||||
|
|
||||||
@uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
|
@uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,6 +58,10 @@ module Banzai
|
||||||
def repository_upload?
|
def repository_upload?
|
||||||
@uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
|
@uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def slug_considered_unsafe?
|
||||||
|
UNSAFE_SLUG_REGEXES.any? { |r| r.match?(@slug) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,8 +70,11 @@ module Banzai
|
||||||
# Build the raw <a> tag just with a link as href and content if
|
# Build the raw <a> tag just with a link as href and content if
|
||||||
# it's originally a link pattern. We shouldn't return a plain text href.
|
# it's originally a link pattern. We shouldn't return a plain text href.
|
||||||
original_link =
|
original_link =
|
||||||
if link_reference == 'true' && href = original_content
|
if link_reference == 'true'
|
||||||
%(<a href="#{href}">#{href}</a>)
|
href = node.attr('href')
|
||||||
|
content = original_content
|
||||||
|
|
||||||
|
%(<a href="#{href}">#{content}</a>)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The reference should be replaced by the original link's content,
|
# The reference should be replaced by the original link's content,
|
||||||
|
|
|
@ -40,6 +40,7 @@ module Gitlab
|
||||||
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
|
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
|
||||||
VERSION = File.read(root.join("VERSION")).strip.freeze
|
VERSION = File.read(root.join("VERSION")).strip.freeze
|
||||||
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
|
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
|
||||||
|
HTTP_PROXY_ENV_VARS = %w(http_proxy https_proxy HTTP_PROXY HTTPS_PROXY).freeze
|
||||||
|
|
||||||
def self.com?
|
def self.com?
|
||||||
# Check `gl_subdomain?` as well to keep parity with gitlab.com
|
# Check `gl_subdomain?` as well to keep parity with gitlab.com
|
||||||
|
@ -62,6 +63,10 @@ module Gitlab
|
||||||
Object.const_defined?(:License)
|
Object.const_defined?(:License)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.http_proxy_env?
|
||||||
|
HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
|
||||||
|
end
|
||||||
|
|
||||||
def self.process_name
|
def self.process_name
|
||||||
return 'sidekiq' if Sidekiq.server?
|
return 'sidekiq' if Sidekiq.server?
|
||||||
return 'console' if defined?(Rails::Console)
|
return 'console' if defined?(Rails::Console)
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module GitRefValidator
|
module GitRefValidator
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
EXPANDED_PREFIXES = %w[refs/heads/ refs/remotes/].freeze
|
||||||
|
DISALLOWED_PREFIXES = %w[-].freeze
|
||||||
|
|
||||||
# Validates a given name against the git reference specification
|
# Validates a given name against the git reference specification
|
||||||
#
|
#
|
||||||
# Returns true for a valid reference name, false otherwise
|
# Returns true for a valid reference name, false otherwise
|
||||||
def validate(ref_name)
|
def validate(ref_name)
|
||||||
not_allowed_prefixes = %w(refs/heads/ refs/remotes/ -)
|
return false if ref_name.start_with?(*(EXPANDED_PREFIXES + DISALLOWED_PREFIXES))
|
||||||
return false if ref_name.start_with?(*not_allowed_prefixes)
|
|
||||||
return false if ref_name == 'HEAD'
|
return false if ref_name == 'HEAD'
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
@ -19,5 +22,21 @@ module Gitlab
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_merge_request_branch(ref_name)
|
||||||
|
return false if ref_name.start_with?(*DISALLOWED_PREFIXES)
|
||||||
|
|
||||||
|
expanded_name = if ref_name.start_with?(*EXPANDED_PREFIXES)
|
||||||
|
ref_name
|
||||||
|
else
|
||||||
|
"refs/heads/#{ref_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
Rugged::Reference.valid_name?(expanded_name)
|
||||||
|
rescue ArgumentError
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
||||||
|
|
||||||
include HTTParty # rubocop:disable Gitlab/HTTParty
|
include HTTParty # rubocop:disable Gitlab/HTTParty
|
||||||
|
|
||||||
connection_adapter ProxyHTTPConnectionAdapter
|
connection_adapter HTTPConnectionAdapter
|
||||||
|
|
||||||
def self.perform_request(http_method, path, options, &block)
|
def self.perform_request(http_method, path, options, &block)
|
||||||
super
|
super
|
||||||
|
|
|
@ -10,17 +10,19 @@
|
||||||
#
|
#
|
||||||
# This option will take precedence over the global setting.
|
# This option will take precedence over the global setting.
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
|
class HTTPConnectionAdapter < HTTParty::ConnectionAdapter
|
||||||
def connection
|
def connection
|
||||||
unless allow_local_requests?
|
|
||||||
begin
|
begin
|
||||||
Gitlab::UrlBlocker.validate!(uri, allow_local_network: false)
|
@uri, hostname = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_local_requests?,
|
||||||
|
allow_localhost: allow_local_requests?,
|
||||||
|
dns_rebind_protection: dns_rebind_protection?)
|
||||||
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
|
raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
super
|
super.tap do |http|
|
||||||
|
http.hostname_override = hostname if hostname
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -29,6 +31,12 @@ module Gitlab
|
||||||
options.fetch(:allow_local_requests, allow_settings_local_requests?)
|
options.fetch(:allow_local_requests, allow_settings_local_requests?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dns_rebind_protection?
|
||||||
|
return false if Gitlab.http_proxy_env?
|
||||||
|
|
||||||
|
Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
def allow_settings_local_requests?
|
def allow_settings_local_requests?
|
||||||
Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
|
Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
|
||||||
end
|
end
|
|
@ -4,6 +4,7 @@ module Gitlab
|
||||||
module ImportExport
|
module ImportExport
|
||||||
class AttributeCleaner
|
class AttributeCleaner
|
||||||
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
|
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
|
||||||
|
PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_html\Z/).freeze
|
||||||
|
|
||||||
def self.clean(*args)
|
def self.clean(*args)
|
||||||
new(*args).clean
|
new(*args).clean
|
||||||
|
@ -24,7 +25,11 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def prohibited_key?(key)
|
def prohibited_key?(key)
|
||||||
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
|
key =~ PROHIBITED_REFERENCES && !permitted_key?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_key?(key)
|
||||||
|
ALLOWED_REFERENCES.include?(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def excluded_key?(key)
|
def excluded_key?(key)
|
||||||
|
|
|
@ -138,6 +138,12 @@ module Gitlab
|
||||||
project
|
project
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_milestones_by_project(milestones)
|
||||||
|
return Milestone.none unless Ability.allowed?(@current_user, :read_milestone, @project)
|
||||||
|
|
||||||
|
milestones.where(project_id: project.id) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
|
||||||
def repository_project_ref
|
def repository_project_ref
|
||||||
@repository_project_ref ||= repository_ref || project.default_branch
|
@repository_project_ref ||= repository_ref || project.default_branch
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,8 +119,10 @@ module Gitlab
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def milestones
|
def milestones
|
||||||
milestones = Milestone.where(project_id: project_ids_relation)
|
milestones = Milestone.search(query)
|
||||||
milestones = milestones.search(query)
|
|
||||||
|
milestones = filter_milestones_by_project(milestones)
|
||||||
|
|
||||||
milestones.reorder('updated_at DESC')
|
milestones.reorder('updated_at DESC')
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
@ -147,6 +149,26 @@ module Gitlab
|
||||||
'projects'
|
'projects'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Filter milestones by authorized projects.
|
||||||
|
# For performance reasons project_id is being plucked
|
||||||
|
# to be used on a smaller query.
|
||||||
|
#
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def filter_milestones_by_project(milestones)
|
||||||
|
project_ids =
|
||||||
|
milestones.where(project_id: project_ids_relation)
|
||||||
|
.select(:project_id).distinct
|
||||||
|
.pluck(:project_id)
|
||||||
|
|
||||||
|
return Milestone.none if project_ids.nil?
|
||||||
|
|
||||||
|
authorized_project_ids_relation =
|
||||||
|
Project.where(id: project_ids).ids_with_milestone_available_for(current_user)
|
||||||
|
|
||||||
|
milestones.where(project_id: authorized_project_ids_relation)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def project_ids_relation
|
def project_ids_relation
|
||||||
limit_projects.select(:id).reorder(nil)
|
limit_projects.select(:id).reorder(nil)
|
||||||
|
|
|
@ -8,38 +8,68 @@ module Gitlab
|
||||||
BlockedUrlError = Class.new(StandardError)
|
BlockedUrlError = Class.new(StandardError)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def validate!(url, ports: [], protocols: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false, enforce_sanitization: false)
|
# Validates the given url according to the constraints specified by arguments.
|
||||||
return true if url.nil?
|
#
|
||||||
|
# ports - Raises error if the given URL port does is not between given ports.
|
||||||
|
# allow_localhost - Raises error if URL resolves to a localhost IP address and argument is true.
|
||||||
|
# allow_local_network - Raises error if URL resolves to a link-local address and argument is true.
|
||||||
|
# ascii_only - Raises error if URL has unicode characters and argument is true.
|
||||||
|
# enforce_user - Raises error if URL user doesn't start with alphanumeric characters and argument is true.
|
||||||
|
# enforce_sanitization - Raises error if URL includes any HTML/CSS/JS tags and argument is true.
|
||||||
|
#
|
||||||
|
# Returns an array with [<uri>, <original-hostname>].
|
||||||
|
# rubocop:disable Metrics/CyclomaticComplexity
|
||||||
|
# rubocop:disable Metrics/ParameterLists
|
||||||
|
def validate!(
|
||||||
|
url,
|
||||||
|
ports: [],
|
||||||
|
protocols: [],
|
||||||
|
allow_localhost: false,
|
||||||
|
allow_local_network: true,
|
||||||
|
ascii_only: false,
|
||||||
|
enforce_user: false,
|
||||||
|
enforce_sanitization: false,
|
||||||
|
dns_rebind_protection: true)
|
||||||
|
# rubocop:enable Metrics/CyclomaticComplexity
|
||||||
|
# rubocop:enable Metrics/ParameterLists
|
||||||
|
|
||||||
|
return [nil, nil] if url.nil?
|
||||||
|
|
||||||
# Param url can be a string, URI or Addressable::URI
|
# Param url can be a string, URI or Addressable::URI
|
||||||
uri = parse_url(url)
|
uri = parse_url(url)
|
||||||
|
|
||||||
validate_html_tags!(uri) if enforce_sanitization
|
validate_html_tags!(uri) if enforce_sanitization
|
||||||
|
|
||||||
# Allow imports from the GitLab instance itself but only from the configured ports
|
hostname = uri.hostname
|
||||||
return true if internal?(uri)
|
|
||||||
|
|
||||||
port = get_port(uri)
|
port = get_port(uri)
|
||||||
|
|
||||||
|
unless internal?(uri)
|
||||||
validate_protocol!(uri.scheme, protocols)
|
validate_protocol!(uri.scheme, protocols)
|
||||||
validate_port!(port, ports) if ports.any?
|
validate_port!(port, ports) if ports.any?
|
||||||
validate_user!(uri.user) if enforce_user
|
validate_user!(uri.user) if enforce_user
|
||||||
validate_hostname!(uri.hostname)
|
validate_hostname!(hostname)
|
||||||
validate_unicode_restriction!(uri) if ascii_only
|
validate_unicode_restriction!(uri) if ascii_only
|
||||||
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
|
addrs_info = Addrinfo.getaddrinfo(hostname, port, nil, :STREAM).map do |addr|
|
||||||
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
|
||||||
end
|
end
|
||||||
rescue SocketError
|
rescue SocketError
|
||||||
return true
|
return [uri, nil]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected_uri_with_hostname = enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
|
||||||
|
|
||||||
|
# Allow url from the GitLab instance itself but only for the configured hostname and ports
|
||||||
|
return protected_uri_with_hostname if internal?(uri)
|
||||||
|
|
||||||
validate_localhost!(addrs_info) unless allow_localhost
|
validate_localhost!(addrs_info) unless allow_localhost
|
||||||
validate_loopback!(addrs_info) unless allow_localhost
|
validate_loopback!(addrs_info) unless allow_localhost
|
||||||
validate_local_network!(addrs_info) unless allow_local_network
|
validate_local_network!(addrs_info) unless allow_local_network
|
||||||
validate_link_local!(addrs_info) unless allow_local_network
|
validate_link_local!(addrs_info) unless allow_local_network
|
||||||
|
|
||||||
true
|
protected_uri_with_hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocked_url?(*args)
|
def blocked_url?(*args)
|
||||||
|
@ -52,6 +82,25 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Returns the given URI with IP address as hostname and the original hostname respectively
|
||||||
|
# in an Array.
|
||||||
|
#
|
||||||
|
# It checks whether the resolved IP address matches with the hostname. If not, it changes
|
||||||
|
# the hostname to the resolved IP address.
|
||||||
|
#
|
||||||
|
# The original hostname is used to validate the SSL, given in that scenario
|
||||||
|
# we'll be making the request to the IP address, instead of using the hostname.
|
||||||
|
def enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
|
||||||
|
address = addrs_info.first
|
||||||
|
ip_address = address&.ip_address
|
||||||
|
|
||||||
|
return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
|
||||||
|
|
||||||
|
uri = uri.dup
|
||||||
|
uri.hostname = ip_address
|
||||||
|
[uri, hostname]
|
||||||
|
end
|
||||||
|
|
||||||
def get_port(uri)
|
def get_port(uri)
|
||||||
uri.port || uri.default_port
|
uri.port || uri.default_port
|
||||||
end
|
end
|
||||||
|
|
|
@ -3235,6 +3235,9 @@ msgstr ""
|
||||||
msgid "Ends at (UTC)"
|
msgid "Ends at (UTC)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enforce DNS rebinding attack protection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enter at least three characters to search"
|
msgid "Enter at least three characters to search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6978,6 +6981,9 @@ msgstr ""
|
||||||
msgid "Resolved all discussions."
|
msgid "Resolved all discussions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Resolves IP addresses once and uses them to submit requests"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Response metrics (AWS ELB)"
|
msgid "Response metrics (AWS ELB)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Projects::Ci::LintsController do
|
describe Projects::Ci::LintsController do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ describe Projects::Ci::LintsController do
|
||||||
|
|
||||||
context 'with a valid gitlab-ci.yml' do
|
context 'with a valid gitlab-ci.yml' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
|
stub_full_request(remote_file_path).to_return(body: remote_file_content)
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
|
|
||||||
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
|
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
|
||||||
|
|
|
@ -173,6 +173,40 @@ describe Projects::MilestonesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#labels' do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
context 'as json' do
|
||||||
|
let!(:guest) { create(:user, username: 'guest1') }
|
||||||
|
let!(:group) { create(:group, :public) }
|
||||||
|
let!(:project) { create(:project, :public, group: group) }
|
||||||
|
let!(:label) { create(:label, title: 'test_label_on_private_issue', project: project) }
|
||||||
|
let!(:confidential_issue) { create(:labeled_issue, confidential: true, project: project, milestone: milestone, labels: [label]) }
|
||||||
|
|
||||||
|
it 'does not render labels of private issues if user has no access' do
|
||||||
|
sign_in(guest)
|
||||||
|
|
||||||
|
get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.content_type).to eq 'application/json'
|
||||||
|
|
||||||
|
expect(json_response['html']).not_to include(label.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does render labels of private issues if user has access' do
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.content_type).to eq 'application/json'
|
||||||
|
|
||||||
|
expect(json_response['html']).to include(label.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'promotion succeeds' do
|
context 'promotion succeeds' do
|
||||||
before do
|
before do
|
||||||
group.add_developer(user)
|
group.add_developer(user)
|
||||||
|
|
|
@ -1,16 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe SentNotificationsController do
|
describe SentNotificationsController do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project, :public) }
|
||||||
let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
|
let(:private_project) { create(:project, :private) }
|
||||||
|
let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) }
|
||||||
|
|
||||||
let(:issue) do
|
let(:issue) do
|
||||||
create(:issue, project: project, author: user) do |issue|
|
create(:issue, project: target_project) do |issue|
|
||||||
issue.subscriptions.create(user: user, project: project, subscribed: true)
|
issue.subscriptions.create(user: user, project: target_project, subscribed: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:confidential_issue) do
|
||||||
|
create(:issue, project: target_project, confidential: true) do |issue|
|
||||||
|
issue.subscriptions.create(user: user, project: target_project, subscribed: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:merge_request) do
|
||||||
|
create(:merge_request, source_project: target_project, target_project: target_project) do |mr|
|
||||||
|
mr.subscriptions.create(user: user, project: target_project, subscribed: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:noteable) { issue }
|
||||||
|
let(:target_project) { project }
|
||||||
|
|
||||||
describe 'GET unsubscribe' do
|
describe 'GET unsubscribe' do
|
||||||
context 'when the user is not logged in' do
|
context 'when the user is not logged in' do
|
||||||
context 'when the force param is passed' do
|
context 'when the force param is passed' do
|
||||||
|
@ -32,22 +50,95 @@ describe SentNotificationsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the force param is not passed' do
|
context 'when the force param is not passed' do
|
||||||
|
render_views
|
||||||
|
|
||||||
before do
|
before do
|
||||||
get(:unsubscribe, params: { id: sent_notification.reply_key })
|
get(:unsubscribe, params: { id: sent_notification.reply_key })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'unsubscribing as anonymous' do
|
||||||
it 'does not unsubscribe the user' do
|
it 'does not unsubscribe the user' do
|
||||||
expect(issue.subscribed?(user, project)).to be_truthy
|
expect(noteable.subscribed?(user, target_project)).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not set the flash message' do
|
it 'does not set the flash message' do
|
||||||
expect(controller).not_to set_flash[:notice]
|
expect(controller).not_to set_flash[:notice]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to the login page' do
|
it 'renders unsubscribe page' do
|
||||||
|
expect(response.status).to eq(200)
|
||||||
expect(response).to render_template :unsubscribe
|
expect(response).to render_template :unsubscribe
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when project is public' do
|
||||||
|
context 'when unsubscribing from issue' do
|
||||||
|
let(:noteable) { issue }
|
||||||
|
|
||||||
|
it 'shows issue title' do
|
||||||
|
expect(response.body).to include(issue.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unsubscribing from confidential issue' do
|
||||||
|
let(:noteable) { confidential_issue }
|
||||||
|
|
||||||
|
it 'does not show issue title' do
|
||||||
|
expect(response.body).not_to include(confidential_issue.title)
|
||||||
|
expect(response.body).to include(confidential_issue.to_reference)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unsubscribing from merge request' do
|
||||||
|
let(:noteable) { merge_request }
|
||||||
|
|
||||||
|
it 'shows merge request title' do
|
||||||
|
expect(response.body).to include(merge_request.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is not public' do
|
||||||
|
let(:target_project) { private_project }
|
||||||
|
|
||||||
|
context 'when unsubscribing from issue' do
|
||||||
|
let(:noteable) { issue }
|
||||||
|
|
||||||
|
it 'shows issue title' do
|
||||||
|
expect(response.body).not_to include(issue.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unsubscribing from confidential issue' do
|
||||||
|
let(:noteable) { confidential_issue }
|
||||||
|
|
||||||
|
it 'does not show issue title' do
|
||||||
|
expect(response.body).not_to include(confidential_issue.title)
|
||||||
|
expect(response.body).to include(confidential_issue.to_reference)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unsubscribing from merge request' do
|
||||||
|
let(:noteable) { merge_request }
|
||||||
|
|
||||||
|
it 'shows merge request title' do
|
||||||
|
expect(response.body).not_to include(merge_request.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unsubscribing as anonymous'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is logged in' do
|
context 'when the user is logged in' do
|
||||||
|
|
|
@ -59,6 +59,25 @@ describe SessionsController do
|
||||||
expect(subject.current_user).to eq user
|
expect(subject.current_user).to eq user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with password authentication disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(password_authentication_enabled_for_web: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not sign in the user' do
|
||||||
|
post(:create, params: { user: user_params })
|
||||||
|
|
||||||
|
expect(@request.env['warden']).not_to be_authenticated
|
||||||
|
expect(subject.current_user).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns status 403' do
|
||||||
|
post(:create, params: { user: user_params })
|
||||||
|
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates an audit log record' do
|
it 'creates an audit log record' do
|
||||||
expect { post(:create, params: { user: user_params }) }.to change { SecurityEvent.count }.by(1)
|
expect { post(:create, params: { user: user_params }) }.to change { SecurityEvent.count }.by(1)
|
||||||
expect(SecurityEvent.last.details[:with]).to eq('standard')
|
expect(SecurityEvent.last.details[:with]).to eq('standard')
|
||||||
|
@ -151,6 +170,19 @@ describe SessionsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with password authentication disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(password_authentication_enabled_for_web: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows 2FA stage of non-password login' do
|
||||||
|
authenticate_2fa(otp_attempt: user.current_otp)
|
||||||
|
|
||||||
|
expect(@request.env['warden']).to be_authenticated
|
||||||
|
expect(subject.current_user).to eq user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# See #14900 issue
|
# See #14900 issue
|
||||||
#
|
#
|
||||||
|
|
|
@ -325,16 +325,19 @@ describe 'Admin updates settings' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'Network page' do
|
context 'Network page' do
|
||||||
it 'Enable outbound requests' do
|
it 'Changes Outbound requests settings' do
|
||||||
visit network_admin_application_settings_path
|
visit network_admin_application_settings_path
|
||||||
|
|
||||||
page.within('.as-outbound') do
|
page.within('.as-outbound') do
|
||||||
check 'Allow requests to the local network from hooks and services'
|
check 'Allow requests to the local network from hooks and services'
|
||||||
|
# Enabled by default
|
||||||
|
uncheck 'Enforce DNS rebinding attack protection'
|
||||||
click_button 'Save changes'
|
click_button 'Save changes'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_content "Application settings saved successfully"
|
expect(page).to have_content "Application settings saved successfully"
|
||||||
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
|
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
|
||||||
|
expect(Gitlab::CurrentSettings.dns_rebinding_protection_enabled).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ describe 'issuable list' do
|
||||||
create(:issue, project: project, author: user)
|
create(:issue, project: project, author: user)
|
||||||
else
|
else
|
||||||
create(:merge_request, source_project: project, source_branch: generate(:branch))
|
create(:merge_request, source_project: project, source_branch: generate(:branch))
|
||||||
source_branch = FFaker::Name.name
|
source_branch = FFaker::Lorem.characters(8)
|
||||||
pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
|
pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
|
||||||
create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
|
create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe 'Import/Export - project export integration test', :js do
|
||||||
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
|
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
|
||||||
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
|
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
|
||||||
|
|
||||||
let(:sensitive_words) { %w[pass secret token key encrypted] }
|
let(:sensitive_words) { %w[pass secret token key encrypted html] }
|
||||||
let(:safe_list) do
|
let(:safe_list) do
|
||||||
{
|
{
|
||||||
token: [ProjectHook, Ci::Trigger, CommitStatus],
|
token: [ProjectHook, Ci::Trigger, CommitStatus],
|
||||||
|
|
|
@ -90,9 +90,12 @@ describe('Job App ', () => {
|
||||||
|
|
||||||
describe('triggered job', () => {
|
describe('triggered job', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const aYearAgo = new Date();
|
||||||
|
aYearAgo.setFullYear(aYearAgo.getFullYear() - 1);
|
||||||
|
|
||||||
mock
|
mock
|
||||||
.onGet(props.endpoint)
|
.onGet(props.endpoint)
|
||||||
.replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
|
.replyOnce(200, Object.assign({}, job, { started: aYearAgo.toISOString() }));
|
||||||
vm = mountComponentWithStore(Component, { props, store });
|
vm = mountComponentWithStore(Component, { props, store });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -70,5 +70,47 @@ describe Banzai::Filter::WikiLinkFilter do
|
||||||
expect(filtered_link.attribute('href').value).to eq(invalid_link)
|
expect(filtered_link.attribute('href').value).to eq(invalid_link)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when the slug is deemed unsafe or invalid" do
|
||||||
|
let(:link) { "alert(1);" }
|
||||||
|
|
||||||
|
invalid_slugs = [
|
||||||
|
"javascript:",
|
||||||
|
"JaVaScRiPt:",
|
||||||
|
"\u0001java\u0003script:",
|
||||||
|
"javascript :",
|
||||||
|
"javascript: ",
|
||||||
|
"javascript : ",
|
||||||
|
":javascript:",
|
||||||
|
"javascript:",
|
||||||
|
"javascript:",
|
||||||
|
"javascript:",
|
||||||
|
"javascript:",
|
||||||
|
"java\0script:",
|
||||||
|
"  javascript:"
|
||||||
|
]
|
||||||
|
|
||||||
|
invalid_slugs.each do |slug|
|
||||||
|
context "with the slug #{slug}" do
|
||||||
|
it "doesn't rewrite a (.) relative link" do
|
||||||
|
filtered_link = filter(
|
||||||
|
"<a href='.#{link}'>Link</a>",
|
||||||
|
project_wiki: wiki,
|
||||||
|
page_slug: slug).children[0]
|
||||||
|
|
||||||
|
expect(filtered_link.attribute('href').value).not_to include(slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't rewrite a (..) relative link" do
|
||||||
|
filtered_link = filter(
|
||||||
|
"<a href='..#{link}'>Link</a>",
|
||||||
|
project_wiki: wiki,
|
||||||
|
page_slug: slug).children[0]
|
||||||
|
|
||||||
|
expect(filtered_link.attribute('href').value).not_to include(slug)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,10 +13,10 @@ describe Banzai::Redactor do
|
||||||
|
|
||||||
it 'redacts an array of documents' do
|
it 'redacts an array of documents' do
|
||||||
doc1 = Nokogiri::HTML
|
doc1 = Nokogiri::HTML
|
||||||
.fragment('<a class="gfm" data-reference-type="issue">foo</a>')
|
.fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">foo</a>')
|
||||||
|
|
||||||
doc2 = Nokogiri::HTML
|
doc2 = Nokogiri::HTML
|
||||||
.fragment('<a class="gfm" data-reference-type="issue">bar</a>')
|
.fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">bar</a>')
|
||||||
|
|
||||||
redacted_data = redactor.redact([doc1, doc2])
|
redacted_data = redactor.redact([doc1, doc2])
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ describe Banzai::Redactor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'replaces redacted reference with inner HTML' do
|
it 'replaces redacted reference with inner HTML' do
|
||||||
doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
|
doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue'>foo</a>")
|
||||||
redactor.redact([doc])
|
redactor.redact([doc])
|
||||||
expect(doc.to_html).to eq('foo')
|
expect(doc.to_html).to eq('foo')
|
||||||
end
|
end
|
||||||
|
@ -35,20 +35,24 @@ describe Banzai::Redactor do
|
||||||
context 'when data-original attribute provided' do
|
context 'when data-original attribute provided' do
|
||||||
let(:original_content) { '<code>foo</code>' }
|
let(:original_content) { '<code>foo</code>' }
|
||||||
it 'replaces redacted reference with original content' do
|
it 'replaces redacted reference with original content' do
|
||||||
doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
|
doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
|
||||||
redactor.redact([doc])
|
redactor.redact([doc])
|
||||||
expect(doc.to_html).to eq(original_content)
|
expect(doc.to_html).to eq(original_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not replace redacted reference with original content if href is given' do
|
||||||
|
html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Marge'>Marge</a>"
|
||||||
|
doc = Nokogiri::HTML.fragment(html)
|
||||||
|
redactor.redact([doc])
|
||||||
|
expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Marge</a>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns <a> tag with original href if it is originally a link reference' do
|
it 'uses the original content as the link content if given' do
|
||||||
href = 'http://localhost:3000'
|
html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Homer'>Marge</a>"
|
||||||
doc = Nokogiri::HTML
|
doc = Nokogiri::HTML.fragment(html)
|
||||||
.fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
|
|
||||||
|
|
||||||
redactor.redact([doc])
|
redactor.redact([doc])
|
||||||
|
expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Homer</a>')
|
||||||
expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,7 +65,7 @@ describe Banzai::Redactor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redacts an issue attached' do
|
it 'redacts an issue attached' do
|
||||||
doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
|
doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
|
||||||
|
|
||||||
redactor.redact([doc])
|
redactor.redact([doc])
|
||||||
|
|
||||||
|
@ -69,7 +73,7 @@ describe Banzai::Redactor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redacts an external issue' do
|
it 'redacts an external issue' do
|
||||||
doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
|
doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
|
||||||
|
|
||||||
redactor.redact([doc])
|
redactor.redact([doc])
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ describe Gitlab::BitbucketImport::Importer do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_omniauth_provider('bitbucket')
|
stub_omniauth_provider('bitbucket')
|
||||||
|
stub_feature_flags(stricter_mr_branch_name: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:statuses) do
|
let(:statuses) do
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::Ci::Config::External::File::Remote do
|
describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
|
let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
|
||||||
let(:params) { { remote: location } }
|
let(:params) { { remote: location } }
|
||||||
let(:remote_file) { described_class.new(params, context) }
|
let(:remote_file) { described_class.new(params, context) }
|
||||||
|
@ -46,7 +48,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
describe "#valid?" do
|
describe "#valid?" do
|
||||||
context 'when is a valid remote url' do
|
context 'when is a valid remote url' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_return(body: remote_file_content)
|
stub_full_request(location).to_return(body: remote_file_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true' do
|
it 'returns true' do
|
||||||
|
@ -92,7 +94,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
describe "#content" do
|
describe "#content" do
|
||||||
context 'with a valid remote file' do
|
context 'with a valid remote file' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_return(body: remote_file_content)
|
stub_full_request(location).to_return(body: remote_file_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the content of the file' do
|
it 'returns the content of the file' do
|
||||||
|
@ -114,7 +116,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
|
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
|
stub_full_request(location).to_raise(SocketError.new('Some HTTP error'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is nil' do
|
it 'is nil' do
|
||||||
|
@ -144,7 +146,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
|
|
||||||
context 'when timeout error has been raised' do
|
context 'when timeout error has been raised' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_timeout
|
stub_full_request(location).to_timeout
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns error message about a timeout' do
|
it 'returns error message about a timeout' do
|
||||||
|
@ -154,7 +156,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
|
|
||||||
context 'when HTTP error has been raised' do
|
context 'when HTTP error has been raised' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error)
|
stub_full_request(location).to_raise(Gitlab::HTTP::Error)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns error message about a HTTP error' do
|
it 'returns error message about a HTTP error' do
|
||||||
|
@ -164,7 +166,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
|
||||||
|
|
||||||
context 'when response has 404 status' do
|
context 'when response has 404 status' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404)
|
stub_full_request(location).to_return(body: remote_file_content, status: 404)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns error message about a timeout' do
|
it 'returns error message about a timeout' do
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::Ci::Config::External::Mapper do
|
describe Gitlab::Ci::Config::External::Mapper do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
set(:project) { create(:project, :repository) }
|
set(:project) { create(:project, :repository) }
|
||||||
set(:user) { create(:user) }
|
set(:user) { create(:user) }
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ describe Gitlab::Ci::Config::External::Mapper do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, remote_url).to_return(body: file_content)
|
stub_full_request(remote_url).to_return(body: file_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#process' do
|
describe '#process' do
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::Ci::Config::External::Processor do
|
describe Gitlab::Ci::Config::External::Processor do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
set(:project) { create(:project, :repository) }
|
set(:project) { create(:project, :repository) }
|
||||||
set(:another_project) { create(:project, :repository) }
|
set(:another_project) { create(:project, :repository) }
|
||||||
set(:user) { create(:user) }
|
set(:user) { create(:user) }
|
||||||
|
@ -42,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do
|
||||||
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
|
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
|
stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
|
@ -75,7 +77,7 @@ describe Gitlab::Ci::Config::External::Processor do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, remote_file).to_return(body: external_file_content)
|
stub_full_request(remote_file).to_return(body: external_file_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'appends the file to the values' do
|
it 'appends the file to the values' do
|
||||||
|
@ -145,7 +147,7 @@ describe Gitlab::Ci::Config::External::Processor do
|
||||||
allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
|
allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
|
||||||
.to receive(:fetch_local_content).and_return(local_file_content)
|
.to receive(:fetch_local_content).and_return(local_file_content)
|
||||||
|
|
||||||
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
|
stub_full_request(remote_file).to_return(body: remote_file_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'appends the files to the values' do
|
it 'appends the files to the values' do
|
||||||
|
@ -191,7 +193,8 @@ describe Gitlab::Ci::Config::External::Processor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'takes precedence' do
|
it 'takes precedence' do
|
||||||
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
|
stub_full_request(remote_file).to_return(body: remote_file_content)
|
||||||
|
|
||||||
expect(processor.perform[:image]).to eq('ruby:2.2')
|
expect(processor.perform[:image]).to eq('ruby:2.2')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -231,7 +234,8 @@ describe Gitlab::Ci::Config::External::Processor do
|
||||||
HEREDOC
|
HEREDOC
|
||||||
end
|
end
|
||||||
|
|
||||||
WebMock.stub_request(:get, 'http://my.domain.com/config.yml').to_return(body: 'remote_build: { script: echo Hello World }')
|
stub_full_request('http://my.domain.com/config.yml')
|
||||||
|
.to_return(body: 'remote_build: { script: echo Hello World }')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when project is public' do
|
context 'when project is public' do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::Ci::Config do
|
describe Gitlab::Ci::Config do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
set(:user) { create(:user) }
|
set(:user) { create(:user) }
|
||||||
|
|
||||||
let(:config) do
|
let(:config) do
|
||||||
|
@ -217,8 +219,7 @@ describe Gitlab::Ci::Config do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, remote_location)
|
stub_full_request(remote_location).to_return(body: remote_file_content)
|
||||||
.to_return(body: remote_file_content)
|
|
||||||
|
|
||||||
allow(project.repository)
|
allow(project.repository)
|
||||||
.to receive(:blob_data_at).and_return(local_file_content)
|
.to receive(:blob_data_at).and_return(local_file_content)
|
||||||
|
|
|
@ -1,31 +1,69 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::GitRefValidator do
|
describe Gitlab::GitRefValidator do
|
||||||
it { expect(described_class.validate('feature/new')).to be_truthy }
|
using RSpec::Parameterized::TableSyntax
|
||||||
it { expect(described_class.validate('implement_@all')).to be_truthy }
|
|
||||||
it { expect(described_class.validate('my_new_feature')).to be_truthy }
|
context '.validate' do
|
||||||
it { expect(described_class.validate('my-branch')).to be_truthy }
|
it { expect(described_class.validate('feature/new')).to be true }
|
||||||
it { expect(described_class.validate('#1')).to be_truthy }
|
it { expect(described_class.validate('implement_@all')).to be true }
|
||||||
it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
|
it { expect(described_class.validate('my_new_feature')).to be true }
|
||||||
it { expect(described_class.validate('feature/~new/')).to be_falsey }
|
it { expect(described_class.validate('my-branch')).to be true }
|
||||||
it { expect(described_class.validate('feature/^new/')).to be_falsey }
|
it { expect(described_class.validate('#1')).to be true }
|
||||||
it { expect(described_class.validate('feature/:new/')).to be_falsey }
|
it { expect(described_class.validate('feature/refs/heads/foo')).to be true }
|
||||||
it { expect(described_class.validate('feature/?new/')).to be_falsey }
|
it { expect(described_class.validate('feature/~new/')).to be false }
|
||||||
it { expect(described_class.validate('feature/*new/')).to be_falsey }
|
it { expect(described_class.validate('feature/^new/')).to be false }
|
||||||
it { expect(described_class.validate('feature/[new/')).to be_falsey }
|
it { expect(described_class.validate('feature/:new/')).to be false }
|
||||||
it { expect(described_class.validate('feature/new/')).to be_falsey }
|
it { expect(described_class.validate('feature/?new/')).to be false }
|
||||||
it { expect(described_class.validate('feature/new.')).to be_falsey }
|
it { expect(described_class.validate('feature/*new/')).to be false }
|
||||||
it { expect(described_class.validate('feature\@{')).to be_falsey }
|
it { expect(described_class.validate('feature/[new/')).to be false }
|
||||||
it { expect(described_class.validate('feature\new')).to be_falsey }
|
it { expect(described_class.validate('feature/new/')).to be false }
|
||||||
it { expect(described_class.validate('feature//new')).to be_falsey }
|
it { expect(described_class.validate('feature/new.')).to be false }
|
||||||
it { expect(described_class.validate('feature new')).to be_falsey }
|
it { expect(described_class.validate('feature\@{')).to be false }
|
||||||
it { expect(described_class.validate('refs/heads/')).to be_falsey }
|
it { expect(described_class.validate('feature\new')).to be false }
|
||||||
it { expect(described_class.validate('refs/remotes/')).to be_falsey }
|
it { expect(described_class.validate('feature//new')).to be false }
|
||||||
it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
|
it { expect(described_class.validate('feature new')).to be false }
|
||||||
it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
|
it { expect(described_class.validate('refs/heads/')).to be false }
|
||||||
it { expect(described_class.validate('-')).to be_falsey }
|
it { expect(described_class.validate('refs/remotes/')).to be false }
|
||||||
it { expect(described_class.validate('-branch')).to be_falsey }
|
it { expect(described_class.validate('refs/heads/feature')).to be false }
|
||||||
it { expect(described_class.validate('.tag')).to be_falsey }
|
it { expect(described_class.validate('refs/remotes/origin')).to be false }
|
||||||
it { expect(described_class.validate('my branch')).to be_falsey }
|
it { expect(described_class.validate('-')).to be false }
|
||||||
it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey }
|
it { expect(described_class.validate('-branch')).to be false }
|
||||||
|
it { expect(described_class.validate('+foo:bar')).to be false }
|
||||||
|
it { expect(described_class.validate('foo:bar')).to be false }
|
||||||
|
it { expect(described_class.validate('.tag')).to be false }
|
||||||
|
it { expect(described_class.validate('my branch')).to be false }
|
||||||
|
it { expect(described_class.validate("\xA0\u0000\xB0")).to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.validate_merge_request_branch' do
|
||||||
|
it { expect(described_class.validate_merge_request_branch('HEAD')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/new')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('implement_@all')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('my_new_feature')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('my-branch')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('#1')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/refs/heads/foo')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/~new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/^new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/:new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/?new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/*new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/[new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/new/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature/new.')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature\@{')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature\new')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature//new')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('feature new')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('refs/heads/master')).to be true }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('refs/heads/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('refs/remotes/')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('-')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('-branch')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('+foo:bar')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('foo:bar')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('.tag')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch('my branch')).to be false }
|
||||||
|
it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
120
spec/lib/gitlab/http_connection_adapter_spec.rb
Normal file
120
spec/lib/gitlab/http_connection_adapter_spec.rb
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::HTTPConnectionAdapter do
|
||||||
|
describe '#connection' do
|
||||||
|
context 'when local requests are not allowed' do
|
||||||
|
it 'sets up the connection' do
|
||||||
|
uri = URI('https://example.org')
|
||||||
|
|
||||||
|
connection = described_class.new(uri).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('93.184.216.34')
|
||||||
|
expect(connection.hostname_override).to eq('example.org')
|
||||||
|
expect(connection.addr_port).to eq('example.org')
|
||||||
|
expect(connection.port).to eq(443)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when it is a request to local address' do
|
||||||
|
uri = URI('http://172.16.0.0/12')
|
||||||
|
|
||||||
|
expect { described_class.new(uri).connection }
|
||||||
|
.to raise_error(Gitlab::HTTP::BlockedUrlError,
|
||||||
|
"URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when it is a request to localhost address' do
|
||||||
|
uri = URI('http://127.0.0.1')
|
||||||
|
|
||||||
|
expect { described_class.new(uri).connection }
|
||||||
|
.to raise_error(Gitlab::HTTP::BlockedUrlError,
|
||||||
|
"URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed")
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when port different from URL scheme is used' do
|
||||||
|
it 'sets up the addr_port accordingly' do
|
||||||
|
uri = URI('https://example.org:8080')
|
||||||
|
|
||||||
|
connection = described_class.new(uri).connection
|
||||||
|
|
||||||
|
expect(connection.address).to eq('93.184.216.34')
|
||||||
|
expect(connection.hostname_override).to eq('example.org')
|
||||||
|
expect(connection.addr_port).to eq('example.org:8080')
|
||||||
|
expect(connection.port).to eq(8080)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when DNS rebinding protection is disabled' do
|
||||||
|
it 'sets up the connection' do
|
||||||
|
stub_application_setting(dns_rebinding_protection_enabled: false)
|
||||||
|
|
||||||
|
uri = URI('https://example.org')
|
||||||
|
|
||||||
|
connection = described_class.new(uri).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('example.org')
|
||||||
|
expect(connection.hostname_override).to eq(nil)
|
||||||
|
expect(connection.addr_port).to eq('example.org')
|
||||||
|
expect(connection.port).to eq(443)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when http(s) environment variable is set' do
|
||||||
|
it 'sets up the connection' do
|
||||||
|
stub_env('https_proxy' => 'https://my.proxy')
|
||||||
|
|
||||||
|
uri = URI('https://example.org')
|
||||||
|
|
||||||
|
connection = described_class.new(uri).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('example.org')
|
||||||
|
expect(connection.hostname_override).to eq(nil)
|
||||||
|
expect(connection.addr_port).to eq('example.org')
|
||||||
|
expect(connection.port).to eq(443)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when local requests are allowed' do
|
||||||
|
it 'sets up the connection' do
|
||||||
|
uri = URI('https://example.org')
|
||||||
|
|
||||||
|
connection = described_class.new(uri, allow_local_requests: true).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('93.184.216.34')
|
||||||
|
expect(connection.hostname_override).to eq('example.org')
|
||||||
|
expect(connection.addr_port).to eq('example.org')
|
||||||
|
expect(connection.port).to eq(443)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets up the connection when it is a local network' do
|
||||||
|
uri = URI('http://172.16.0.0/12')
|
||||||
|
|
||||||
|
connection = described_class.new(uri, allow_local_requests: true).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('172.16.0.0')
|
||||||
|
expect(connection.hostname_override).to be(nil)
|
||||||
|
expect(connection.addr_port).to eq('172.16.0.0')
|
||||||
|
expect(connection.port).to eq(80)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets up the connection when it is localhost' do
|
||||||
|
uri = URI('http://127.0.0.1')
|
||||||
|
|
||||||
|
connection = described_class.new(uri, allow_local_requests: true).connection
|
||||||
|
|
||||||
|
expect(connection).to be_a(Net::HTTP)
|
||||||
|
expect(connection.address).to eq('127.0.0.1')
|
||||||
|
expect(connection.hostname_override).to be(nil)
|
||||||
|
expect(connection.addr_port).to eq('127.0.0.1')
|
||||||
|
expect(connection.port).to eq(80)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,28 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::HTTP do
|
describe Gitlab::HTTP do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
|
context 'when allow_local_requests' do
|
||||||
|
it 'sends the request to the correct URI' do
|
||||||
|
stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200)
|
||||||
|
|
||||||
|
described_class.get('https://example.org:8080', allow_local_requests: false)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not allow_local_requests' do
|
||||||
|
it 'sends the request to the correct URI' do
|
||||||
|
stub_full_request('https://example.org:8080')
|
||||||
|
|
||||||
|
described_class.get('https://example.org:8080', allow_local_requests: true)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'allow_local_requests_from_hooks_and_services is' do
|
describe 'allow_local_requests_from_hooks_and_services is' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
|
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
|
||||||
|
@ -21,6 +43,8 @@ describe Gitlab::HTTP do
|
||||||
|
|
||||||
context 'if allow_local_requests set to true' do
|
context 'if allow_local_requests set to true' do
|
||||||
it 'override the global value and allow requests to localhost or private network' do
|
it 'override the global value and allow requests to localhost or private network' do
|
||||||
|
stub_full_request('http://localhost:3003')
|
||||||
|
|
||||||
expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
|
expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,6 +56,8 @@ describe Gitlab::HTTP do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allow requests to localhost' do
|
it 'allow requests to localhost' do
|
||||||
|
stub_full_request('http://localhost:3003')
|
||||||
|
|
||||||
expect { described_class.get('http://localhost:3003') }.not_to raise_error
|
expect { described_class.get('http://localhost:3003') }.not_to raise_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -49,7 +75,7 @@ describe Gitlab::HTTP do
|
||||||
|
|
||||||
describe 'handle redirect loops' do
|
describe 'handle redirect loops' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:any, "http://example.org").to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
|
stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'handles GET requests' do
|
it 'handles GET requests' do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
|
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:example_url) { 'http://www.example.com' }
|
let(:example_url) { 'http://www.example.com' }
|
||||||
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
|
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
|
||||||
let!(:project) { create(:project, :with_export) }
|
let!(:project) { create(:project, :with_export) }
|
||||||
|
@ -35,7 +37,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
|
||||||
|
|
||||||
context 'when upload fails' do
|
context 'when upload fails' do
|
||||||
it 'stores the export error' do
|
it 'stores the export error' do
|
||||||
stub_request(:post, example_url).to_return(status: [404, 'Page not found'])
|
stub_full_request(example_url, method: :post).to_return(status: [404, 'Page not found'])
|
||||||
|
|
||||||
strategy.execute(user, project)
|
strategy.execute(user, project)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,11 @@ describe Gitlab::ImportExport::AttributeCleaner do
|
||||||
'notid' => 99,
|
'notid' => 99,
|
||||||
'import_source' => 'whatever',
|
'import_source' => 'whatever',
|
||||||
'import_type' => 'whatever',
|
'import_type' => 'whatever',
|
||||||
'non_existent_attr' => 'whatever'
|
'non_existent_attr' => 'whatever',
|
||||||
|
'some_html' => '<p>dodgy html</p>',
|
||||||
|
'legit_html' => '<p>legit html</p>',
|
||||||
|
'_html' => '<p>perfectly ordinary html</p>',
|
||||||
|
'cached_markdown_version' => 12345
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,8 @@
|
||||||
{
|
{
|
||||||
"id": 351,
|
"id": 351,
|
||||||
"note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
|
"note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
|
||||||
|
"note_html": "<p>something else entirely</p>",
|
||||||
|
"cached_markdown_version": 917504,
|
||||||
"noteable_type": "Issue",
|
"noteable_type": "Issue",
|
||||||
"author_id": 26,
|
"author_id": 26,
|
||||||
"created_at": "2016-06-14T15:02:47.770Z",
|
"created_at": "2016-06-14T15:02:47.770Z",
|
||||||
|
@ -2363,6 +2365,8 @@
|
||||||
{
|
{
|
||||||
"id": 671,
|
"id": 671,
|
||||||
"note": "Sit voluptatibus eveniet architecto quidem.",
|
"note": "Sit voluptatibus eveniet architecto quidem.",
|
||||||
|
"note_html": "<p>something else entirely</p>",
|
||||||
|
"cached_markdown_version": 917504,
|
||||||
"noteable_type": "MergeRequest",
|
"noteable_type": "MergeRequest",
|
||||||
"author_id": 26,
|
"author_id": 26,
|
||||||
"created_at": "2016-06-14T15:02:56.632Z",
|
"created_at": "2016-06-14T15:02:56.632Z",
|
||||||
|
|
|
@ -58,6 +58,26 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
||||||
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
|
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when importing a project with cached_markdown_version and note_html' do
|
||||||
|
context 'for an Issue' do
|
||||||
|
it 'does not import note_html' do
|
||||||
|
note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi'
|
||||||
|
issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first
|
||||||
|
|
||||||
|
expect(issue_note.note_html).to match(/#{note_content}/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for a Merge Request' do
|
||||||
|
it 'does not import note_html' do
|
||||||
|
note_content = 'Sit voluptatibus eveniet architecto quidem'
|
||||||
|
merge_request_note = MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{note_content}/)}.first
|
||||||
|
|
||||||
|
expect(merge_request_note.note_html).to match(/#{note_content}/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a valid pipeline note' do
|
it 'creates a valid pipeline note' do
|
||||||
expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
|
expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
|
||||||
end
|
end
|
||||||
|
|
|
@ -256,4 +256,28 @@ describe Gitlab::SearchResults do
|
||||||
|
|
||||||
expect(results.objects('merge_requests')).not_to include merge_request
|
expect(results.objects('merge_requests')).not_to include merge_request
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'milestones' do
|
||||||
|
it 'returns correct set of milestones' do
|
||||||
|
private_project_1 = create(:project, :private)
|
||||||
|
private_project_2 = create(:project, :private)
|
||||||
|
internal_project = create(:project, :internal)
|
||||||
|
public_project_1 = create(:project, :public)
|
||||||
|
public_project_2 = create(:project, :public, :issues_disabled, :merge_requests_disabled)
|
||||||
|
private_project_1.add_developer(user)
|
||||||
|
# milestones that should not be visible
|
||||||
|
create(:milestone, project: private_project_2, title: 'Private project without access milestone')
|
||||||
|
create(:milestone, project: public_project_2, title: 'Public project with milestones disabled milestone')
|
||||||
|
# milestones that should be visible
|
||||||
|
milestone_1 = create(:milestone, project: private_project_1, title: 'Private project with access milestone', state: 'closed')
|
||||||
|
milestone_2 = create(:milestone, project: internal_project, title: 'Internal project milestone')
|
||||||
|
milestone_3 = create(:milestone, project: public_project_1, title: 'Public project with milestones enabled milestone')
|
||||||
|
# Global search scope takes user authorized projects, internal projects and public projects.
|
||||||
|
limit_projects = ProjectsFinder.new(current_user: user).execute
|
||||||
|
|
||||||
|
milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones')
|
||||||
|
|
||||||
|
expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,87 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Gitlab::UrlBlocker do
|
describe Gitlab::UrlBlocker do
|
||||||
|
describe '#validate!' do
|
||||||
|
context 'when URI is nil' do
|
||||||
|
let(:import_url) { nil }
|
||||||
|
|
||||||
|
it 'returns no URI and hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url)
|
||||||
|
|
||||||
|
expect(uri).to be(nil)
|
||||||
|
expect(hostname).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when URI is internal' do
|
||||||
|
let(:import_url) { 'http://localhost' }
|
||||||
|
|
||||||
|
it 'returns URI and no hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('http://[::1]'))
|
||||||
|
expect(hostname).to eq('localhost')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the URL hostname is a domain' do
|
||||||
|
let(:import_url) { 'https://example.org' }
|
||||||
|
|
||||||
|
it 'returns URI and hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
|
||||||
|
expect(hostname).to eq('example.org')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the URL hostname is an IP address' do
|
||||||
|
let(:import_url) { 'https://93.184.216.34' }
|
||||||
|
|
||||||
|
it 'returns URI and no hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
|
||||||
|
expect(hostname).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'disabled DNS rebinding protection' do
|
||||||
|
context 'when URI is internal' do
|
||||||
|
let(:import_url) { 'http://localhost' }
|
||||||
|
|
||||||
|
it 'returns URI and no hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('http://localhost'))
|
||||||
|
expect(hostname).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the URL hostname is a domain' do
|
||||||
|
let(:import_url) { 'https://example.org' }
|
||||||
|
|
||||||
|
it 'returns URI and no hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('https://example.org'))
|
||||||
|
expect(hostname).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the URL hostname is an IP address' do
|
||||||
|
let(:import_url) { 'https://93.184.216.34' }
|
||||||
|
|
||||||
|
it 'returns URI and no hostname' do
|
||||||
|
uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
|
||||||
|
|
||||||
|
expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
|
||||||
|
expect(hostname).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#blocked_url?' do
|
describe '#blocked_url?' do
|
||||||
let(:ports) { Project::VALID_IMPORT_PORTS }
|
let(:ports) { Project::VALID_IMPORT_PORTS }
|
||||||
|
|
||||||
|
@ -208,7 +289,7 @@ describe Gitlab::UrlBlocker do
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_domain_resolv(domain, ip)
|
def stub_domain_resolv(domain, ip)
|
||||||
address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)
|
address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false)
|
||||||
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
|
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
|
||||||
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
|
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,4 +109,34 @@ describe Gitlab do
|
||||||
expect(described_class.ee?).to eq(false)
|
expect(described_class.ee?).to eq(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.http_proxy_env?' do
|
||||||
|
it 'returns true when lower case https' do
|
||||||
|
stub_env('https_proxy', 'https://my.proxy')
|
||||||
|
|
||||||
|
expect(described_class.http_proxy_env?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true when upper case https' do
|
||||||
|
stub_env('HTTPS_PROXY', 'https://my.proxy')
|
||||||
|
|
||||||
|
expect(described_class.http_proxy_env?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true when lower case http' do
|
||||||
|
stub_env('http_proxy', 'http://my.proxy')
|
||||||
|
|
||||||
|
expect(described_class.http_proxy_env?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true when upper case http' do
|
||||||
|
stub_env('HTTP_PROXY', 'http://my.proxy')
|
||||||
|
|
||||||
|
expect(described_class.http_proxy_env?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false when not set' do
|
||||||
|
expect(described_class.http_proxy_env?).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe Mattermost::Session, type: :request do
|
describe Mattermost::Session, type: :request do
|
||||||
include ExclusiveLeaseHelpers
|
include ExclusiveLeaseHelpers
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ describe Mattermost::Session, type: :request do
|
||||||
let(:location) { 'http://location.tld' }
|
let(:location) { 'http://location.tld' }
|
||||||
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
|
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
|
||||||
let!(:stub) do
|
let!(:stub) do
|
||||||
WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login")
|
stub_full_request("#{mattermost_url}/oauth/gitlab/login")
|
||||||
.to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
|
.to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ describe Mattermost::Session, type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete")
|
stub_full_request("#{mattermost_url}/signup/gitlab/complete")
|
||||||
.with(query: hash_including({ 'state' => state }))
|
.with(query: hash_including({ 'state' => state }))
|
||||||
.to_return do |request|
|
.to_return do |request|
|
||||||
post "/oauth/token",
|
post "/oauth/token",
|
||||||
|
@ -80,7 +81,7 @@ describe Mattermost::Session, type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout")
|
stub_full_request("#{mattermost_url}/api/v4/users/logout", method: :post)
|
||||||
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
|
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ describe Clusters::Applications::Knative do
|
||||||
subject { knative.install_command }
|
subject { knative.install_command }
|
||||||
|
|
||||||
it 'is initialized with latest version' do
|
it 'is initialized with latest version' do
|
||||||
expect(subject.version).to eq('0.3.0')
|
expect(subject.version).to eq('0.5.0')
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a command'
|
it_behaves_like 'a command'
|
||||||
|
|
|
@ -150,6 +150,42 @@ describe MergeRequest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for branch' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(stricter_mr_branch_name: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:branch_name, :valid) do
|
||||||
|
'foo' | true
|
||||||
|
'foo:bar' | false
|
||||||
|
'+foo:bar' | false
|
||||||
|
'foo bar' | false
|
||||||
|
'-foo' | false
|
||||||
|
'HEAD' | true
|
||||||
|
'refs/heads/master' | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it "validates source_branch" do
|
||||||
|
subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
|
||||||
|
|
||||||
|
subject.valid?
|
||||||
|
|
||||||
|
expect(subject.errors.added?(:source_branch)).to eq(!valid)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "validates target_branch" do
|
||||||
|
subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
|
||||||
|
|
||||||
|
subject.valid?
|
||||||
|
|
||||||
|
expect(subject.errors.added?(:target_branch)).to eq(!valid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'for forks' do
|
context 'for forks' do
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
let(:fork1) { fork_project(project) }
|
let(:fork1) { fork_project(project) }
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe AssemblaService do
|
describe AssemblaService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
describe "Associations" do
|
describe "Associations" do
|
||||||
it { is_expected.to belong_to :project }
|
it { is_expected.to belong_to :project }
|
||||||
it { is_expected.to have_one :service_hook }
|
it { is_expected.to have_one :service_hook }
|
||||||
|
@ -23,12 +25,12 @@ describe AssemblaService do
|
||||||
)
|
)
|
||||||
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
|
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
|
||||||
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
|
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
|
||||||
WebMock.stub_request(:post, @api_url)
|
stub_full_request(@api_url, method: :post)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calls Assembla API" do
|
it "calls Assembla API" do
|
||||||
@assembla_service.execute(@sample_data)
|
@assembla_service.execute(@sample_data)
|
||||||
expect(WebMock).to have_requested(:post, @api_url).with(
|
expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
|
||||||
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
|
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
|
||||||
).once
|
).once
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe BambooService, :use_clean_rails_memory_store_caching do
|
describe BambooService, :use_clean_rails_memory_store_caching do
|
||||||
include ReactiveCachingHelpers
|
include ReactiveCachingHelpers
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
|
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
|
||||||
|
|
||||||
|
@ -257,7 +258,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_bamboo_request(url, status, body)
|
def stub_bamboo_request(url, status, body)
|
||||||
WebMock.stub_request(:get, url).to_return(
|
stub_full_request(url).to_return(
|
||||||
status: status,
|
status: status,
|
||||||
headers: { 'Content-Type' => 'application/json' },
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
body: body
|
body: body
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe BuildkiteService, :use_clean_rails_memory_store_caching do
|
describe BuildkiteService, :use_clean_rails_memory_store_caching do
|
||||||
include ReactiveCachingHelpers
|
include ReactiveCachingHelpers
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
|
|
||||||
|
@ -110,10 +111,9 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
|
||||||
body ||= %q({"status":"success"})
|
body ||= %q({"status":"success"})
|
||||||
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
|
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
|
||||||
|
|
||||||
WebMock.stub_request(:get, buildkite_full_url).to_return(
|
stub_full_request(buildkite_full_url)
|
||||||
status: status,
|
.to_return(status: status,
|
||||||
headers: { 'Content-Type' => 'application/json' },
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
body: body
|
body: body)
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe CampfireService do
|
describe CampfireService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
describe 'Associations' do
|
describe 'Associations' do
|
||||||
it { is_expected.to belong_to :project }
|
it { is_expected.to belong_to :project }
|
||||||
it { is_expected.to have_one :service_hook }
|
it { is_expected.to have_one :service_hook }
|
||||||
|
@ -49,39 +51,37 @@ describe CampfireService do
|
||||||
it "calls Campfire API to get a list of rooms and speak in a room" do
|
it "calls Campfire API to get a list of rooms and speak in a room" do
|
||||||
# make sure a valid list of rooms is returned
|
# make sure a valid list of rooms is returned
|
||||||
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
|
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
|
||||||
WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
|
|
||||||
|
stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
|
||||||
body: body,
|
body: body,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: @headers
|
headers: @headers
|
||||||
)
|
)
|
||||||
|
|
||||||
# stub the speak request with the room id found in the previous request's response
|
# stub the speak request with the room id found in the previous request's response
|
||||||
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
|
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
|
||||||
WebMock.stub_request(:post, speak_url).with(basic_auth: @auth)
|
stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
|
||||||
|
|
||||||
@campfire_service.execute(@sample_data)
|
@campfire_service.execute(@sample_data)
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:get, @rooms_url).once
|
expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
|
||||||
expect(WebMock).to have_requested(:post, speak_url).with(
|
expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
|
||||||
body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
|
.with(body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/).once
|
||||||
).once
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
|
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
|
||||||
# return a list of rooms that do not contain a room named 'test-room'
|
# return a list of rooms that do not contain a room named 'test-room'
|
||||||
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
|
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
|
||||||
WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
|
stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
|
||||||
body: body,
|
body: body,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: @headers
|
headers: @headers
|
||||||
)
|
)
|
||||||
# we want to make sure no request is sent to the /speak endpoint, here is a basic
|
|
||||||
# regexp that matches this endpoint
|
|
||||||
speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
|
|
||||||
|
|
||||||
@campfire_service.execute(@sample_data)
|
@campfire_service.execute(@sample_data)
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:get, @rooms_url).once
|
expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
|
||||||
expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
|
expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe PivotaltrackerService do
|
describe PivotaltrackerService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
describe 'Associations' do
|
describe 'Associations' do
|
||||||
it { is_expected.to belong_to :project }
|
it { is_expected.to belong_to :project }
|
||||||
it { is_expected.to have_one :service_hook }
|
it { is_expected.to have_one :service_hook }
|
||||||
|
@ -53,12 +55,12 @@ describe PivotaltrackerService do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:post, url)
|
stub_full_request(url, method: :post)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'posts correct message' do
|
it 'posts correct message' do
|
||||||
service.execute(push_data)
|
service.execute(push_data)
|
||||||
expect(WebMock).to have_requested(:post, url).with(
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
|
||||||
body: {
|
body: {
|
||||||
'source_commit' => {
|
'source_commit' => {
|
||||||
'commit_id' => '21c12ea',
|
'commit_id' => '21c12ea',
|
||||||
|
@ -85,14 +87,14 @@ describe PivotaltrackerService do
|
||||||
service.execute(push_data(branch: 'master'))
|
service.execute(push_data(branch: 'master'))
|
||||||
service.execute(push_data(branch: 'v10'))
|
service.execute(push_data(branch: 'v10'))
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:post, url).twice
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).twice
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not post message if branch is not in the list' do
|
it 'does not post message if branch is not in the list' do
|
||||||
service.execute(push_data(branch: 'mas'))
|
service.execute(push_data(branch: 'mas'))
|
||||||
service.execute(push_data(branch: 'v11'))
|
service.execute(push_data(branch: 'v11'))
|
||||||
|
|
||||||
expect(WebMock).not_to have_requested(:post, url)
|
expect(WebMock).not_to have_requested(:post, stubbed_hostname(url))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe PushoverService do
|
describe PushoverService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
describe 'Associations' do
|
describe 'Associations' do
|
||||||
it { is_expected.to belong_to :project }
|
it { is_expected.to belong_to :project }
|
||||||
it { is_expected.to have_one :service_hook }
|
it { is_expected.to have_one :service_hook }
|
||||||
|
@ -57,13 +59,13 @@ describe PushoverService do
|
||||||
sound: sound
|
sound: sound
|
||||||
)
|
)
|
||||||
|
|
||||||
WebMock.stub_request(:post, api_url)
|
stub_full_request(api_url, method: :post, ip_address: '8.8.8.8')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls Pushover API' do
|
it 'calls Pushover API' do
|
||||||
pushover.execute(sample_data)
|
pushover.execute(sample_data)
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:post, api_url).once
|
expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe TeamcityService, :use_clean_rails_memory_store_caching do
|
describe TeamcityService, :use_clean_rails_memory_store_caching do
|
||||||
include ReactiveCachingHelpers
|
include ReactiveCachingHelpers
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
|
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
|
||||||
|
|
||||||
|
@ -212,7 +213,7 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do
|
||||||
|
|
||||||
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
|
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
|
||||||
|
|
||||||
WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return(
|
stub_full_request(teamcity_full_url).with(basic_auth: auth).to_return(
|
||||||
status: status,
|
status: status,
|
||||||
headers: { 'Content-Type' => 'application/json' },
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
body: body
|
body: body
|
||||||
|
|
|
@ -214,6 +214,13 @@ describe Project do
|
||||||
expect(project2).not_to be_valid
|
expect(project2).not_to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'validates the visibility' do
|
||||||
|
expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original
|
||||||
|
expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original
|
||||||
|
|
||||||
|
create(:project)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'wiki path conflict' do
|
describe 'wiki path conflict' do
|
||||||
context "when the new path has been used by the wiki of other Project" do
|
context "when the new path has been used by the wiki of other Project" do
|
||||||
it 'has an error on the name attribute' do
|
it 'has an error on the name attribute' do
|
||||||
|
@ -3163,6 +3170,23 @@ describe Project do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.ids_with_milestone_available_for' do
|
||||||
|
let!(:user) { create(:user) }
|
||||||
|
|
||||||
|
it 'returns project ids with milestones available for user' do
|
||||||
|
project_1 = create(:project, :public, :merge_requests_disabled, :issues_disabled)
|
||||||
|
project_2 = create(:project, :public, :merge_requests_disabled)
|
||||||
|
project_3 = create(:project, :public, :issues_disabled)
|
||||||
|
project_4 = create(:project, :public)
|
||||||
|
project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
|
||||||
|
|
||||||
|
project_ids = described_class.ids_with_milestone_available_for(user).pluck(:id)
|
||||||
|
|
||||||
|
expect(project_ids).to include(project_2.id, project_3.id)
|
||||||
|
expect(project_ids).not_to include(project_1.id, project_4.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.with_feature_available_for_user' do
|
describe '.with_feature_available_for_user' do
|
||||||
let!(:user) { create(:user) }
|
let!(:user) { create(:user) }
|
||||||
let!(:feature) { MergeRequest }
|
let!(:feature) { MergeRequest }
|
||||||
|
|
|
@ -70,13 +70,32 @@ describe API::Search do
|
||||||
context 'for milestones scope' do
|
context 'for milestones scope' do
|
||||||
before do
|
before do
|
||||||
create(:milestone, project: project, title: 'awesome milestone')
|
create(:milestone, project: project, title: 'awesome milestone')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can read project milestones' do
|
||||||
|
before do
|
||||||
get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
|
get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
|
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user cannot read project milestones' do
|
||||||
|
before do
|
||||||
|
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||||
|
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty array' do
|
||||||
|
get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
|
||||||
|
|
||||||
|
milestones = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(milestones).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'for users scope' do
|
context 'for users scope' do
|
||||||
before do
|
before do
|
||||||
create(:user, name: 'billy')
|
create(:user, name: 'billy')
|
||||||
|
@ -318,13 +337,32 @@ describe API::Search do
|
||||||
context 'for milestones scope' do
|
context 'for milestones scope' do
|
||||||
before do
|
before do
|
||||||
create(:milestone, project: project, title: 'awesome milestone')
|
create(:milestone, project: project, title: 'awesome milestone')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can read milestones' do
|
||||||
|
before do
|
||||||
get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
|
get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
|
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user cannot read project milestones' do
|
||||||
|
before do
|
||||||
|
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||||
|
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty array' do
|
||||||
|
get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
|
||||||
|
|
||||||
|
milestones = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(milestones).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'for users scope' do
|
context 'for users scope' do
|
||||||
before do
|
before do
|
||||||
user1 = create(:user, name: 'billy')
|
user1 = create(:user, name: 'billy')
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe API::SystemHooks do
|
describe API::SystemHooks do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:admin) { create(:admin) }
|
let(:admin) { create(:admin) }
|
||||||
let!(:hook) { create(:system_hook, url: "http://example.com") }
|
let!(:hook) { create(:system_hook, url: "http://example.com") }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:post, hook.url)
|
stub_full_request(hook.url, method: :post)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /hooks" do
|
describe "GET /hooks" do
|
||||||
|
@ -68,6 +70,8 @@ describe API::SystemHooks do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets default values for events' do
|
it 'sets default values for events' do
|
||||||
|
stub_full_request('http://mep.mep', method: :post)
|
||||||
|
|
||||||
post api('/hooks', admin), params: { url: 'http://mep.mep' }
|
post api('/hooks', admin), params: { url: 'http://mep.mep' }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(201)
|
expect(response).to have_gitlab_http_status(201)
|
||||||
|
@ -78,6 +82,8 @@ describe API::SystemHooks do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets explicit values for events' do
|
it 'sets explicit values for events' do
|
||||||
|
stub_full_request('http://mep.mep', method: :post)
|
||||||
|
|
||||||
post api('/hooks', admin),
|
post api('/hooks', admin),
|
||||||
params: {
|
params: {
|
||||||
url: 'http://mep.mep',
|
url: 'http://mep.mep',
|
||||||
|
|
|
@ -922,7 +922,7 @@ describe Ci::CreatePipelineService do
|
||||||
let(:merge_request) do
|
let(:merge_request) do
|
||||||
create(:merge_request,
|
create(:merge_request,
|
||||||
source_project: project,
|
source_project: project,
|
||||||
source_branch: ref_name,
|
source_branch: Gitlab::Git.ref_name(ref_name),
|
||||||
target_project: project,
|
target_project: project,
|
||||||
target_branch: 'master')
|
target_branch: 'master')
|
||||||
end
|
end
|
||||||
|
@ -953,7 +953,7 @@ describe Ci::CreatePipelineService do
|
||||||
let(:merge_request) do
|
let(:merge_request) do
|
||||||
create(:merge_request,
|
create(:merge_request,
|
||||||
source_project: project,
|
source_project: project,
|
||||||
source_branch: ref_name,
|
source_branch: Gitlab::Git.ref_name(ref_name),
|
||||||
target_project: project,
|
target_project: project,
|
||||||
target_branch: 'master')
|
target_branch: 'master')
|
||||||
end
|
end
|
||||||
|
@ -982,7 +982,7 @@ describe Ci::CreatePipelineService do
|
||||||
let(:merge_request) do
|
let(:merge_request) do
|
||||||
create(:merge_request,
|
create(:merge_request,
|
||||||
source_project: project,
|
source_project: project,
|
||||||
source_branch: ref_name,
|
source_branch: Gitlab::Git.ref_name(ref_name),
|
||||||
target_project: project,
|
target_project: project,
|
||||||
target_branch: 'master')
|
target_branch: 'master')
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Projects::LfsPointers::LfsDownloadService do
|
describe Projects::LfsPointers::LfsDownloadService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
let(:lfs_content) { SecureRandom.random_bytes(10) }
|
let(:lfs_content) { SecureRandom.random_bytes(10) }
|
||||||
let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
|
let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
|
||||||
|
@ -61,7 +63,7 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'when file download succeeds' do
|
context 'when file download succeeds' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
|
stub_full_request(download_link).to_return(body: lfs_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'lfs object is created'
|
it_behaves_like 'lfs object is created'
|
||||||
|
@ -103,7 +105,7 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
let(:size) { 1 }
|
let(:size) { 1 }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
|
stub_full_request(download_link).to_return(body: lfs_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'no lfs object is created'
|
it_behaves_like 'no lfs object is created'
|
||||||
|
@ -117,7 +119,7 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
|
|
||||||
context 'when downloaded lfs file has a different oid' do
|
context 'when downloaded lfs file has a different oid' do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
|
stub_full_request(download_link).to_return(body: lfs_content)
|
||||||
allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
|
allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -135,7 +137,7 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
|
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
|
stub_full_request(download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'the request adds authorization headers' do
|
it 'the request adds authorization headers' do
|
||||||
|
@ -148,7 +150,7 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
let(:local_request_setting) { true }
|
let(:local_request_setting) { true }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
|
stub_full_request(download_link, ip_address: '192.168.2.120').to_return(body: lfs_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'lfs object is created'
|
it_behaves_like 'lfs object is created'
|
||||||
|
@ -172,7 +174,8 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
|
stub_full_request(download_link, ip_address: '192.168.2.120')
|
||||||
|
.to_return(status: 301, headers: { 'Location' => redirect_link })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'no lfs object is created'
|
it_behaves_like 'no lfs object is created'
|
||||||
|
@ -183,8 +186,8 @@ describe Projects::LfsPointers::LfsDownloadService do
|
||||||
let(:redirect_link) { "http://example.com/"}
|
let(:redirect_link) { "http://example.com/"}
|
||||||
|
|
||||||
before do
|
before do
|
||||||
WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
|
stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
|
||||||
WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
|
stub_full_request(redirect_link).to_return(body: lfs_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'lfs object is created'
|
it_behaves_like 'lfs object is created'
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe SubmitUsagePingService do
|
describe SubmitUsagePingService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
context 'when usage ping is disabled' do
|
context 'when usage ping is disabled' do
|
||||||
before do
|
before do
|
||||||
stub_application_setting(usage_ping_enabled: false)
|
stub_application_setting(usage_ping_enabled: false)
|
||||||
|
@ -97,7 +99,7 @@ describe SubmitUsagePingService do
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_response(body)
|
def stub_response(body)
|
||||||
stub_request(:post, 'https://version.gitlab.com/usage_data')
|
stub_full_request('https://version.gitlab.com/usage_data', method: :post)
|
||||||
.to_return(
|
.to_return(
|
||||||
headers: { 'Content-Type' => 'application/json' },
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
body: body.to_json
|
body: body.to_json
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe WebHookService do
|
describe WebHookService do
|
||||||
|
include StubRequests
|
||||||
|
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
let(:project_hook) { create(:project_hook) }
|
let(:project_hook) { create(:project_hook) }
|
||||||
let(:headers) do
|
let(:headers) do
|
||||||
|
@ -65,11 +67,11 @@ describe WebHookService do
|
||||||
let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
|
let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
|
||||||
|
|
||||||
it 'uses the credentials' do
|
it 'uses the credentials' do
|
||||||
WebMock.stub_request(:post, url)
|
stub_full_request(url, method: :post)
|
||||||
|
|
||||||
service_instance.execute
|
service_instance.execute
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:post, url).with(
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
|
||||||
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
|
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
|
||||||
).once
|
).once
|
||||||
end
|
end
|
||||||
|
@ -80,11 +82,11 @@ describe WebHookService do
|
||||||
let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
|
let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
|
||||||
|
|
||||||
it 'uses the credentials anyways' do
|
it 'uses the credentials anyways' do
|
||||||
WebMock.stub_request(:post, url)
|
stub_full_request(url, method: :post)
|
||||||
|
|
||||||
service_instance.execute
|
service_instance.execute
|
||||||
|
|
||||||
expect(WebMock).to have_requested(:post, url).with(
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
|
||||||
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
|
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
|
||||||
).once
|
).once
|
||||||
end
|
end
|
||||||
|
|
40
spec/support/helpers/stub_requests.rb
Normal file
40
spec/support/helpers/stub_requests.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
module StubRequests
|
||||||
|
IP_ADDRESS_STUB = '8.8.8.9'.freeze
|
||||||
|
|
||||||
|
# Fully stubs a request using WebMock class. This class also
|
||||||
|
# stubs the IP address the URL is translated to (DNS lookup).
|
||||||
|
#
|
||||||
|
# It expects the final request to go to the `ip_address` instead the given url.
|
||||||
|
# That's primarily a DNS rebind attack prevention of Gitlab::HTTP
|
||||||
|
# (see: Gitlab::UrlBlocker).
|
||||||
|
#
|
||||||
|
def stub_full_request(url, ip_address: IP_ADDRESS_STUB, port: 80, method: :get)
|
||||||
|
stub_dns(url, ip_address: ip_address, port: port)
|
||||||
|
|
||||||
|
url = stubbed_hostname(url, hostname: ip_address)
|
||||||
|
WebMock.stub_request(method, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_dns(url, ip_address:, port: 80)
|
||||||
|
url = parse_url(url)
|
||||||
|
socket = Socket.sockaddr_in(port, ip_address)
|
||||||
|
addr = Addrinfo.new(socket)
|
||||||
|
|
||||||
|
# See Gitlab::UrlBlocker
|
||||||
|
allow(Addrinfo).to receive(:getaddrinfo)
|
||||||
|
.with(url.hostname, url.port, nil, :STREAM)
|
||||||
|
.and_return([addr])
|
||||||
|
end
|
||||||
|
|
||||||
|
def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
|
||||||
|
url = parse_url(url)
|
||||||
|
url.hostname = hostname
|
||||||
|
url.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_url(url)
|
||||||
|
url.is_a?(URI) ? url : URI(url)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue