diff options
author | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2022-02-03 23:40:30 +0000 |
---|---|---|
committer | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2022-02-03 23:40:30 +0000 |
commit | 871acce6c1e3de3f1ab09f0dcb6bcf822bd138f0 (patch) | |
tree | 0f6970f168ddba8b32efcada5c7407676bb3da15 | |
parent | d81e007aec64d5e0aade50cb18847fa9fb6d0e77 (diff) | |
parent | bb9e85e5e3750dd3acf3bf99a455b0a562ec6ace (diff) | |
download | gitlab-ce-14-5-stable.tar.gz |
Merge remote-tracking branch 'dev/14-5-stable' into 14-5-stable14-5-stable
88 files changed, 1020 insertions, 201 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4d35620fc..3bdf921efca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 14.5.4 (2022-02-03) + +No changes. + ## 14.5.3 (2022-01-11) No changes. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7f0423b3c48..b51f608d943 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -14.5.3
\ No newline at end of file +14.5.4
\ No newline at end of file @@ -166,7 +166,7 @@ gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '~> 0.0.12' gem 'asciidoctor-kroki', '~> 0.5.0', require: false -gem 'rouge', '~> 3.26.1' +gem 'rouge', '~> 3.27.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.11.4' diff --git a/Gemfile.lock b/Gemfile.lock index b54874e9d80..4519c375466 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1053,7 +1053,7 @@ GEM rexml (3.2.5) rinku (2.0.0) rotp (6.2.0) - rouge (3.26.1) + rouge (3.27.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1597,7 +1597,7 @@ DEPENDENCIES responders (~> 3.0) retriable (~> 3.1.2) rexml (~> 3.2.5) - rouge (~> 3.26.1) + rouge (~> 3.27.0) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 5.0.1) @@ -1 +1 @@ -14.5.3
\ No newline at end of file +14.5.4
\ No newline at end of file diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index 1472adf458b..b39720c6094 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -1,4 +1,3 @@ -import { escape } from 'lodash'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class CreateItemDropdown { @@ -37,14 +36,14 @@ export default class CreateItemDropdown { }, selectable: true, toggleLabel(selected) { - return selected && 'id' in selected ? escape(selected.title) : this.defaultToggleLabel; + return selected && 'id' in selected ? selected.title : this.defaultToggleLabel; }, fieldName: this.fieldName, text(item) { - return escape(item.text); + return item.text; }, id(item) { - return escape(item.id); + return item.id; }, onFilter: this.toggleCreateNewButton.bind(this), clicked: (options) => { diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index ca02ee18dd1..2d1d8845e41 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -30,6 +30,9 @@ export default { }, safeHtmlConfig: { ADD_TAGS: ['use'], // to support icon SVGs + FORBID_TAGS: ['style'], + FORBID_ATTR: ['style'], + ALLOW_DATA_ATTR: false, }, }; </script> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue index bcbeec72961..4c5ce8134ee 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue @@ -299,7 +299,7 @@ export default { <delete-package @start="track($options.trackingActions.DELETE_PACKAGE_TRACKING_ACTION)" - @end="navigateToListWithSuccessModal" + @success="navigateToListWithSuccessModal" > <template #default="{ deletePackage }"> <gl-modal diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue index 7a85fd3052e..7f7edd42c42 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue @@ -23,6 +23,12 @@ export default { successMessage: DELETE_PACKAGE_SUCCESS_MESSAGE, }, methods: { + errorMessageFrom(error) { + if (typeof error === 'string') { + return error; + } + return error?.message || this.$options.i18n.errorMessage; + }, async deletePackage(packageEntity) { try { this.$emit('start'); @@ -44,9 +50,10 @@ export default { type: 'success', }); } + this.$emit('success'); } catch (error) { createFlash({ - message: this.$options.i18n.errorMessage, + message: this.errorMessageFrom(error), type: 'warning', captureError: true, error, diff --git a/app/controllers/jira_connect/users_controller.rb b/app/controllers/jira_connect/users_controller.rb index 569dc42fed3..a37c68de299 100644 --- a/app/controllers/jira_connect/users_controller.rb +++ b/app/controllers/jira_connect/users_controller.rb @@ -5,7 +5,17 @@ class JiraConnect::UsersController < ApplicationController layout 'signup_onboarding' + before_action :verify_return_to_url, only: [:show] + def show @jira_app_link = params.delete(:return_to) end + + private + + def verify_return_to_url + return unless params[:return_to].present? + + params.delete(:return_to) unless Integrations::Jira.valid_jira_cloud_url?(params[:return_to]) + end end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 8054ecbd502..2b4ce615090 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -74,7 +74,7 @@ class UsersFinder def by_search(users) return users unless params[:search].present? - users.search(params[:search]) + users.search(params[:search], with_private_emails: current_user&.admin?) end def by_blocked(users) diff --git a/app/graphql/mutations/packages/destroy.rb b/app/graphql/mutations/packages/destroy.rb index 979a54da6bd..50b5190cbf6 100644 --- a/app/graphql/mutations/packages/destroy.rb +++ b/app/graphql/mutations/packages/destroy.rb @@ -4,6 +4,7 @@ module Mutations module Packages class Destroy < ::Mutations::BaseMutation graphql_name 'DestroyPackage' + description 'Destroys a package and its related package files. Restricted to packages with a small number of files' authorize :destroy_package diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index b6cb9cd3302..66aa08b5221 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -427,6 +427,12 @@ module Types ::Security::CiConfiguration::SastParserService.new(object).configuration end + def service_desk_address + return unless Ability.allowed?(current_user, :admin_issue, project) + + object.service_desk_address + end + def tag_list object.topic_list end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index af5796d682f..3c917a9870d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -371,6 +371,10 @@ class ApplicationSetting < ApplicationRecord validates :invisible_captcha_enabled, inclusion: { in: [true, false], message: _('must be a boolean value') } + validates :max_package_files_for_package_destruction, + allow_nil: false, + numericality: { only_integer: true, greater_than: 0 } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb new file mode 100644 index 00000000000..11dc8a76a2b --- /dev/null +++ b/app/models/concerns/integrations/enable_ssl_verification.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Integrations + module EnableSslVerification + extend ActiveSupport::Concern + + prepended do + boolean_accessor :enable_ssl_verification + end + + def initialize_properties + super + + self.enable_ssl_verification = true if new_record? && enable_ssl_verification.nil? + end + + def fields + super.tap do |fields| + url_index = fields.index { |field| field[:name].ends_with?('_url') } + insert_index = url_index ? url_index + 1 : -1 + + fields.insert(insert_index, { + type: 'checkbox', + name: 'enable_ssl_verification', + title: s_('Integrations|SSL verification'), + checkbox_label: s_('Integrations|Enable SSL verification'), + help: s_('Integrations|Clear if using a self-signed certificate.') + }) + end + end + end +end diff --git a/app/models/concerns/integrations/has_web_hook.rb b/app/models/concerns/integrations/has_web_hook.rb index dabe7152b18..bc28c32695c 100644 --- a/app/models/concerns/integrations/has_web_hook.rb +++ b/app/models/concerns/integrations/has_web_hook.rb @@ -15,7 +15,11 @@ module Integrations # Return whether the webhook should use SSL verification. def hook_ssl_verification - true + if respond_to?(:enable_ssl_verification) + enable_ssl_verification + else + true + end end # Create or update the webhook, raising an exception if it cannot be saved. diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 0774b84b69f..57767c63cf4 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -4,6 +4,7 @@ module Integrations class Bamboo < BaseCi include ActionView::Helpers::UrlHelper include ReactivelyCached + prepend EnableSslVerification prop_accessor :bamboo_url, :build_key, :username, :password @@ -162,7 +163,7 @@ module Integrations end def build_get_params(query_params) - params = { verify: false, query: query_params } + params = { verify: enable_ssl_verification, query: query_params } return params if username.blank? && password.blank? query_params[:os_authType] = 'basic' diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 9fad3a42647..90593d78a5d 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -137,7 +137,7 @@ module Integrations end def request_options - { verify: false, extra_log_info: { project_id: project_id } } + { extra_log_info: { project_id: project_id } } end end end diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index 856d14c022d..3c18e5d8732 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -5,10 +5,12 @@ module Integrations include HasWebHook include PushDataValidations include ReactivelyCached + prepend EnableSslVerification extend Gitlab::Utils::Override + DRONE_SAAS_HOSTNAME = 'cloud.drone.io' + prop_accessor :drone_url, :token - boolean_accessor :enable_ssl_verification validates :drone_url, presence: true, public_url: true, if: :activated? validates :token, presence: true, if: :activated? @@ -95,8 +97,7 @@ module Integrations def fields [ { type: 'text', name: 'token', help: s_('ProjectService|Token for the Drone project.'), required: true }, - { type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true }, - { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" } + { type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true } ] end @@ -105,15 +106,24 @@ module Integrations [drone_url, "/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join end - override :hook_ssl_verification - def hook_ssl_verification - !!enable_ssl_verification - end - override :update_web_hook! def update_web_hook! # If using a service template, project may not be available super if project end + + def enable_ssl_verification + original_value = Gitlab::Utils.to_boolean(properties['enable_ssl_verification']) + original_value.nil? ? (new_record? || url_is_saas?) : original_value + end + + private + + def url_is_saas? + parsed_url = Addressable::URI.parse(drone_url) + parsed_url&.scheme == 'https' && parsed_url.hostname == DRONE_SAAS_HOSTNAME + rescue Addressable::URI::InvalidURIError + false + end end end diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index e5c1d5ad0d7..5ea92170c26 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -4,6 +4,7 @@ module Integrations class Jenkins < BaseCi include HasWebHook include ActionView::Helpers::UrlHelper + prepend EnableSslVerification extend Gitlab::Utils::Override prop_accessor :jenkins_url, :project_name, :username, :password diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 42c291abf55..51995e50841 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -56,6 +56,12 @@ module Integrations @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/ end + def self.valid_jira_cloud_url?(url) + return false unless url.present? + + !!URI(url).hostname&.end_with?(JIRA_CLOUD_HOST) + end + def initialize_properties {} end @@ -569,7 +575,7 @@ module Integrations end def jira_cloud? - server_info['deploymentType'] == 'Cloud' || URI(client_url).hostname.end_with?(JIRA_CLOUD_HOST) + server_info['deploymentType'] == 'Cloud' || self.class.valid_jira_cloud_url?(client_url) end def set_deployment_type_from_url @@ -582,7 +588,7 @@ module Integrations # we can only assume it's either Cloud or Server # based on the URL being *.atlassian.net - if URI(client_url).hostname.end_with?(JIRA_CLOUD_HOST) + if self.class.valid_jira_cloud_url?(client_url) data_fields.deployment_cloud! else data_fields.deployment_server! diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb index 7359be83d4f..568fb609a44 100644 --- a/app/models/integrations/mock_ci.rb +++ b/app/models/integrations/mock_ci.rb @@ -3,6 +3,8 @@ # For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service module Integrations class MockCi < BaseCi + prepend EnableSslVerification + ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze prop_accessor :mock_service_url @@ -55,7 +57,7 @@ module Integrations # # => 'running' # def commit_status(sha, ref) - response = Gitlab::HTTP.get(commit_status_path(sha), verify: false, use_read_total_timeout: true) + response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification, use_read_total_timeout: true) read_commit_status(response) rescue Errno::ECONNREFUSED :error @@ -68,19 +70,16 @@ module Integrations end def read_commit_status(response) - return :error unless response.code == 200 || response.code == 404 - - status = if response.code == 404 - 'pending' - else - response['status'] - end + return :pending if response.code == 404 + return :error unless response.code == 200 - if status.present? && ALLOWED_STATES.include?(status) - status - else - :error + begin + status = Gitlab::Json.parse(response.body).try(:fetch, 'status', nil) + return status if ALLOWED_STATES.include?(status) + rescue JSON::ParserError end + + :error end def testable? diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index 008b591c304..f0f83f118d7 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -4,6 +4,9 @@ module Integrations class Teamcity < BaseCi include PushDataValidations include ReactivelyCached + prepend EnableSslVerification + + TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze prop_accessor :teamcity_url, :build_type, :username, :password @@ -104,8 +107,20 @@ module Integrations end end + def enable_ssl_verification + original_value = Gitlab::Utils.to_boolean(properties['enable_ssl_verification']) + original_value.nil? ? (new_record? || url_is_saas?) : original_value + end + private + def url_is_saas? + parsed_url = Addressable::URI.parse(teamcity_url) + parsed_url&.scheme == 'https' && parsed_url.hostname.match?(TEAMCITY_SAAS_HOSTNAME) + rescue Addressable::URI::InvalidURIError + false + end + def execute_push(data) branch = Gitlab::Git.ref_name(data[:ref]) post_to_build_queue(data, branch) if push_valid?(data) @@ -155,7 +170,7 @@ module Integrations end def get_path(path) - Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true) + Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true) end def post_to_build_queue(data, branch) @@ -165,6 +180,7 @@ module Integrations "<buildType id=#{build_type.encode(xml: :attr)}/>"\ '</build>', headers: { 'Content-type' => 'application/xml' }, + verify: enable_ssl_verification, basic_auth: basic_auth, use_read_total_timeout: true ) diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 749b9dce97c..1847aaba717 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -10,7 +10,7 @@ class SystemNoteMetadata < ApplicationRecord # in the same project (i.e. with the same permissions) TYPES_WITH_CROSS_REFERENCES = %w[ commit cross_reference - close duplicate + closed duplicate moved merge label milestone relate unrelate diff --git a/app/models/user.rb b/app/models/user.rb index 3ab5b7ee364..618197e4bba 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -655,6 +655,7 @@ class User < ApplicationRecord # This method uses ILIKE on PostgreSQL. # # query - The search query as a String + # with_private_emails - include private emails in search # # Returns an ActiveRecord::Relation. def search(query, **options) @@ -667,14 +668,16 @@ class User < ApplicationRecord CASE WHEN users.name = :query THEN 0 WHEN users.username = :query THEN 1 - WHEN users.email = :query THEN 2 + WHEN users.public_email = :query THEN 2 ELSE 3 END SQL sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query])) - search_with_secondary_emails(query).reorder(sanitized_order_sql, :name) + scope = options[:with_private_emails] ? search_with_secondary_emails(query) : search_with_public_emails(query) + + scope.reorder(sanitized_order_sql, :name) end # Limits the result set to users _not_ in the given query/list of IDs. @@ -689,6 +692,18 @@ class User < ApplicationRecord reorder(:name) end + def search_with_public_emails(query) + return none if query.blank? + + query = query.downcase + + where( + fuzzy_arel_match(:name, query) + .or(fuzzy_arel_match(:username, query)) + .or(arel_table[:public_email].eq(query)) + ) + end + def search_without_secondary_emails(query) return none if query.blank? diff --git a/app/services/packages/destroy_package_service.rb b/app/services/packages/destroy_package_service.rb index 697f1fa3ac8..2238d459bed 100644 --- a/app/services/packages/destroy_package_service.rb +++ b/app/services/packages/destroy_package_service.rb @@ -7,6 +7,10 @@ module Packages def execute return service_response_error("You don't have access to this package", 403) unless user_can_delete_package? + if too_many_package_files? + return service_response_error("It's not possible to delete a package with more than #{max_package_files} #{'file'.pluralize(max_package_files)}.", 400) + end + package.destroy! package.sync_maven_metadata(current_user) @@ -26,6 +30,14 @@ module Packages ServiceResponse.success(message: message) end + def too_many_package_files? + max_package_files < package.package_files.limit(max_package_files + 1).count + end + + def max_package_files + ::Gitlab::CurrentSettings.current_application_settings.max_package_files_for_package_destruction + end + def user_can_delete_package? can?(current_user, :destroy_package, package.project) end diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index df801311aaf..f48e02ab4b5 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -13,23 +13,5 @@ module ProtectedBranches def after_execute(*) # overridden in EE::ProtectedBranches module end - - def filtered_params - return unless params - - params[:name] = sanitize_branch_name(params[:name]) if params[:name].present? - params - end - - private - - def sanitize_branch_name(name) - name = CGI.unescapeHTML(name) - name = Sanitize.fragment(name) - - # Sanitize.fragment escapes HTML chars, so unescape again to allow names - # like `feature->master` - CGI.unescapeHTML(name) - end end end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index ea494dd4426..dada449989a 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -21,7 +21,7 @@ module ProtectedBranches end def protected_branch - @protected_branch ||= project.protected_branches.new(filtered_params) + @protected_branch ||= project.protected_branches.new(params) end end end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 40e9a286af9..1e70f2d9793 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -8,7 +8,7 @@ module ProtectedBranches old_merge_access_levels = protected_branch.merge_access_levels.map(&:clone) old_push_access_levels = protected_branch.push_access_levels.map(&:clone) - if protected_branch.update(filtered_params) + if protected_branch.update(params) after_execute(protected_branch: protected_branch, old_merge_access_levels: old_merge_access_levels, old_push_access_levels: old_push_access_levels) end diff --git a/app/services/protected_tags/create_service.rb b/app/services/protected_tags/create_service.rb index 9aff55986b2..65303f21a4a 100644 --- a/app/services/protected_tags/create_service.rb +++ b/app/services/protected_tags/create_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ProtectedTags - class CreateService < BaseService + class CreateService < ::BaseService attr_reader :protected_tag def execute diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb index 3eb5f4955ee..283aa8882c5 100644 --- a/app/services/protected_tags/update_service.rb +++ b/app/services/protected_tags/update_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ProtectedTags - class UpdateService < BaseService + class UpdateService < ::BaseService def execute(protected_tag) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index eee223ff63c..2ae950f3b0d 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -6,7 +6,7 @@ = render "devise/shared/error_messages", resource: resource .form-group = f.label :email - = f.email_field :email, class: "form-control gl-form-input", required: true, title: _('Please provide a valid email address.'), value: nil + = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', title: _('Please provide a valid email address.'), value: nil %div - if recaptcha_enabled? diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 7bbde4a39c7..d5372862128 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -5,7 +5,7 @@ = render "devise/shared/error_messages", resource: resource .form-group = f.label :email - = f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.') + = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.') .form-text.text-muted = _('Requires your primary GitLab email address.') diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml index 06a86c2465f..3b079ea00b7 100644 --- a/app/views/groups/_import_group_from_another_instance_panel.html.haml +++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml @@ -26,6 +26,7 @@ = s_('GroupsNew|Navigate to user settings to find your %{link_start}personal access token%{link_end}.').html_safe % { link_start: pat_link_start, link_end: '</a>'.html_safe } = f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8', required: true, + autocomplete: 'off', title: s_('GroupsNew|Please fill in your personal access token.'), id: 'import_gitlab_token', data: { qa_selector: 'import_gitlab_token' } diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 3097a9fbc03..4f51bb69b8c 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -2,6 +2,7 @@ require 'json' require 'socket' +require 'resolv' class IrkerWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker @@ -43,9 +44,18 @@ class IrkerWorker # rubocop:disable Scalability/IdempotentWorker private def start_connection(irker_server, irker_port) + ip_address = Resolv.getaddress(irker_server) + # handle IP6 addresses + domain = Resolv::IPv6::Regex.match?(ip_address) ? "[#{ip_address}]" : ip_address + begin - @socket = TCPSocket.new irker_server, irker_port - rescue Errno::ECONNREFUSED => e + Gitlab::UrlBlocker.validate!( + "irc://#{domain}", + allow_localhost: allow_local_requests?, + allow_local_network: allow_local_requests?, + schemes: ['irc']) + @socket = TCPSocket.new ip_address, irker_port + rescue Errno::ECONNREFUSED, Gitlab::UrlBlocker::BlockedUrlError => e logger.fatal "Can't connect to Irker daemon: #{e}" return false end @@ -53,6 +63,10 @@ class IrkerWorker # rubocop:disable Scalability/IdempotentWorker true end + def allow_local_requests? + Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? + end + def send_to_irker(privmsg) to_send = { to: @channels, privmsg: privmsg } diff --git a/db/migrate/20220113135449_add_package_files_limit_to_application_settings.rb b/db/migrate/20220113135449_add_package_files_limit_to_application_settings.rb new file mode 100644 index 00000000000..6d3deacdda3 --- /dev/null +++ b/db/migrate/20220113135449_add_package_files_limit_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPackageFilesLimitToApplicationSettings < Gitlab::Database::Migration[1.0] + def change + add_column :application_settings, :max_package_files_for_package_destruction, :smallint, default: 100, null: false + end +end diff --git a/db/migrate/20220113135924_add_application_settings_package_files_limit_constraints.rb b/db/migrate/20220113135924_add_application_settings_package_files_limit_constraints.rb new file mode 100644 index 00000000000..65fbccbd1ae --- /dev/null +++ b/db/migrate/20220113135924_add_application_settings_package_files_limit_constraints.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddApplicationSettingsPackageFilesLimitConstraints < Gitlab::Database::Migration[1.0] + CONSTRAINT_NAME = 'app_settings_max_package_files_for_package_destruction_positive' + + disable_ddl_transaction! + + def up + add_check_constraint :application_settings, 'max_package_files_for_package_destruction > 0', CONSTRAINT_NAME + end + + def down + remove_check_constraint :application_settings, CONSTRAINT_NAME + end +end diff --git a/db/schema_migrations/20220113135449 b/db/schema_migrations/20220113135449 new file mode 100644 index 00000000000..57e6ede94b5 --- /dev/null +++ b/db/schema_migrations/20220113135449 @@ -0,0 +1 @@ +46796379175ce9343907234d3ae14a417442c7c5ebbfcf6987db1d759eca2c3a
\ No newline at end of file diff --git a/db/schema_migrations/20220113135924 b/db/schema_migrations/20220113135924 new file mode 100644 index 00000000000..41328a43c37 --- /dev/null +++ b/db/schema_migrations/20220113135924 @@ -0,0 +1 @@ +2a230758c13111c9e3738794008c31a3608dda2f0d071fbde0ad3cd782d29162
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5373aa7a31e..db513473a27 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10454,9 +10454,11 @@ CREATE TABLE application_settings ( sentry_dsn text, sentry_clientside_dsn text, sentry_environment text, + max_package_files_for_package_destruction smallint DEFAULT 100 NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), + CONSTRAINT app_settings_max_package_files_for_package_destruction_positive CHECK ((max_package_files_for_package_destruction > 0)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), CONSTRAINT app_settings_yaml_max_depth_positive CHECK ((max_yaml_depth > 0)), CONSTRAINT app_settings_yaml_max_size_positive CHECK ((max_yaml_size_bytes > 0)), diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 905b6d418a9..61b8f78d287 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2142,6 +2142,8 @@ Input type: `DestroyNoteInput` ### `Mutation.destroyPackage` +Destroys a package and its related package files. Restricted to packages with a small number of files. + Input type: `DestroyPackageInput` #### Arguments @@ -15491,7 +15493,7 @@ Represents an issue link of a vulnerability. | Name | Type | Description | | ---- | ---- | ----------- | | <a id="vulnerabilityissuelinkid"></a>`id` | [`ID!`](#id) | GraphQL ID of the vulnerability. | -| <a id="vulnerabilityissuelinkissue"></a>`issue` | [`Issue!`](#issue) | Issue attached to issue link. | +| <a id="vulnerabilityissuelinkissue"></a>`issue` | [`Issue`](#issue) | Issue attached to issue link. | | <a id="vulnerabilityissuelinklinktype"></a>`linkType` | [`VulnerabilityIssueLinkType!`](#vulnerabilityissuelinktype) | Type of the issue link. | ### `VulnerabilityLink` diff --git a/doc/api/integrations.md b/doc/api/integrations.md index 8f57e915b4e..933484551e0 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -165,6 +165,7 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `bamboo_url` | string | true | Bamboo root URL. For example, `https://bamboo.example.com`. | +| `enable_ssl_verification` | boolean | false | Enable SSL verification. Defaults to true (enabled). | | `build_key` | string | true | Bamboo build plan key like KEY | | `username` | string | true | A user with API access, if applicable | | `password` | string | true | Password of the user | @@ -519,7 +520,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `token` | string | true | Drone CI project specific token | | `drone_url` | string | true | `http://drone.example.com` | -| `enable_ssl_verification` | boolean | false | Enable SSL verification | +| `enable_ssl_verification` | boolean | false | Enable SSL verification. Defaults to true (enabled). | | `push_events` | boolean | false | Enable notifications for push events | | `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `tag_push_events` | boolean | false | Enable notifications for tag push events | @@ -1394,6 +1395,7 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `teamcity_url` | string | true | TeamCity root URL. For example, `https://teamcity.example.com` | +| `enable_ssl_verification` | boolean | false | Enable SSL verification. Defaults to true (enabled). | | `build_type` | string | true | Build configuration ID | | `username` | string | true | A user with permissions to trigger a manual build | | `password` | string | true | The password of the user | @@ -1432,6 +1434,7 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `jenkins_url` | string | true | Jenkins URL like `http://jenkins.example.com`. | +| `enable_ssl_verification` | boolean | false | Enable SSL verification. Defaults to true (enabled). | | `project_name` | string | true | The URL-friendly project name. Example: `my_project_name`. | | `username` | string | false | Username for authentication with the Jenkins server, if authentication is required by the server. | | `password` | string | false | Password for authentication with the Jenkins server, if authentication is required by the server. | @@ -1511,6 +1514,7 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `mock_service_url` | string | true | `http://localhost:4004` | +| `enable_ssl_verification` | boolean | false | Enable SSL verification. Defaults to true (enabled). | ### Disable MockCI integration diff --git a/doc/api/members.md b/doc/api/members.md index ce276487f21..4eccd4f61de 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -254,11 +254,11 @@ respectively. GET /groups/:id/billable_members ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- |--------------------------------------------------------------------------------------------------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user | -| `search` | string | no | A query string to search for group members by name, username, or email. | -| `sort` | string | no | A query string containing parameters that specify the sort attribute and order. See supported values below.| +| `search` | string | no | A query string to search for group members by name, username, or public email. | +| `sort` | string | no | A query string containing parameters that specify the sort attribute and order. See supported values below. | The supported values for the `sort` attribute are: diff --git a/doc/api/packages.md b/doc/api/packages.md index a75b2e376fa..b34bcf6afb4 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -333,6 +333,9 @@ By default, the `GET` request returns 20 results, because the API is [paginated] > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9623) in GitLab 11.9. +WARNING: +This endpoint is restricted to the limit set in [Updating the package files limit](#updating-the-package-files-limit). + Deletes a project package. ```plaintext @@ -352,6 +355,17 @@ Can return the following status codes: - `204 No Content`, if the package was deleted successfully. - `404 Not Found`, if the package was not found. +- `400 Bad Request`, if the package has too many package files. + +### Updating the package files limit + +For scalability reasons, deleting a package is limited to packages with less than 100 files. An +administrator can update this limit through the [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session). +For example, this command updates this limit to 50 files: + +```ruby +ApplicationSetting.last.update(max_package_files_for_package_destruction: 50) +``` ## Delete a package file diff --git a/doc/api/status_checks.md b/doc/api/status_checks.md index 0c72ee37348..6f6ac1ccd74 100644 --- a/doc/api/status_checks.md +++ b/doc/api/status_checks.md @@ -45,6 +45,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/status_checks ## Set status of an external status check For a single merge request, use the API to inform GitLab that a merge request has passed a check by an external service. +To set the status of an external check, the personal access token used must belong to a user with at least the developer role on the target project of the merge request. ```plaintext POST /projects/:id/merge_requests/:merge_request_iid/status_check_responses diff --git a/doc/api/users.md b/doc/api/users.md index d8effc4d38f..bb2c9cb2bad 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -39,7 +39,7 @@ GET /users ] ``` -You can also search for users by name, username, primary email, or secondary email, by using `?search=`. For example. `/users?search=John`. +You can also search for users by name, username or public email by using `?search=`. For example. `/users?search=John`. In addition, you can lookup users by username: diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index e7fdb6645a5..a67d7717a82 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -197,6 +197,12 @@ module API desc: 'Bamboo root URL like https://bamboo.example.com' }, { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification' + }, + { required: true, name: :build_key, type: String, @@ -360,7 +366,7 @@ module API required: false, name: :enable_ssl_verification, type: Boolean, - desc: 'Enable SSL verification for communication' + desc: 'Enable SSL verification' } ], 'emails-on-push' => [ @@ -460,6 +466,12 @@ module API desc: 'Jenkins root URL like https://jenkins.example.com' }, { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification' + }, + { required: true, name: :project_name, type: String, @@ -741,6 +753,12 @@ module API desc: 'TeamCity root URL like https://teamcity.example.com' }, { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification' + }, + { required: true, name: :build_type, type: String, diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 54c0a0628a7..b06d0685021 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -71,7 +71,11 @@ module API .new(user_project, params[:package_id]).execute destroy_conditionally!(package) do |package| - ::Packages::DestroyPackageService.new(container: package, current_user: current_user).execute + result = ::Packages::DestroyPackageService.new(container: package, current_user: current_user).execute + + unless result.success? + render_api_error!(result.message, result.http_status) + end end end end diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb index 8f5ad9981e5..e07cbfe8d85 100644 --- a/lib/banzai/filter/blockquote_fence_filter.rb +++ b/lib/banzai/filter/blockquote_fence_filter.rb @@ -6,7 +6,7 @@ module Banzai REGEX = %r{ #{::Gitlab::Regex.markdown_code_or_html_blocks} | - (?: + (?=^>>>\ *\n.*\n>>>\ *$)(?: # Blockquote: # >>> # Anything, including code and HTML blocks diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index f092e03046a..48228ede684 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -148,9 +148,17 @@ module Gitlab unless allow_local_network validate_local_network(address_info) validate_link_local(address_info) + validate_shared_address(address_info) end end + def validate_shared_address(addrs_info) + netmask = IPAddr.new('100.64.0.0/10') + return unless addrs_info.any? { |addr| netmask.include?(addr.ip_address) } + + raise BlockedUrlError, "Requests to the shared address space are not allowed" + end + def get_port(uri) uri.port || uri.default_port end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1e290fa6c62..38fcd11c91d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18601,6 +18601,9 @@ msgstr "" msgid "Integrations|Browser limitations" msgstr "" +msgid "Integrations|Clear if using a self-signed certificate." +msgstr "" + msgid "Integrations|Comment detail:" msgstr "" @@ -18631,6 +18634,9 @@ msgstr "" msgid "Integrations|Enable GitLab.com slash commands in a Slack workspace." msgstr "" +msgid "Integrations|Enable SSL verification" +msgstr "" + msgid "Integrations|Enable comments" msgstr "" @@ -18709,6 +18715,9 @@ msgstr "" msgid "Integrations|Return to GitLab for Jira" msgstr "" +msgid "Integrations|SSL verification" +msgstr "" + msgid "Integrations|Save settings?" msgstr "" diff --git a/package.json b/package.json index 5016d75f74c..abaa38a5c59 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "dateformat": "^5.0.1", "deckar01-task_list": "^2.3.1", "diff": "^3.4.0", - "dompurify": "^2.3.3", + "dompurify": "^2.3.4", "dropzone": "^4.2.0", "editorconfig": "^0.15.3", "emoji-regex": "^7.0.3", @@ -148,7 +148,7 @@ "lowlight": "^1.20.0", "marked": "^0.3.12", "mathjax": "3", - "mermaid": "^8.13.4", + "mermaid": "^8.13.10", "minimatch": "^3.0.4", "monaco-editor": "^0.25.2", "monaco-editor-webpack-plugin": "^4.0.0", diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index 38e829bafcc..36bd2b76214 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -129,7 +129,7 @@ RSpec.describe 'Groups > Members > Manage members' do find('[data-testid="members-token-select-input"]').set('undisclosed_email@gitlab.com') wait_for_requests - expect(page).to have_content("Jane 'invisible' Doe") + expect(page).to have_content('Invite "undisclosed_email@gitlab.com" by email') end context 'when Invite Members modal is disabled' do @@ -155,7 +155,7 @@ RSpec.describe 'Groups > Members > Manage members' do select_input.send_keys('undisclosed_email@gitlab.com') wait_for_requests - expect(page).to have_content("Jane 'invisible' Doe") + expect(page).to have_content('Invite "undisclosed_email@gitlab.com" by email') end end diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb index be85d73d777..4e98062e8b2 100644 --- a/spec/features/issues/notes_on_issues_spec.rb +++ b/spec/features/issues/notes_on_issues_spec.rb @@ -91,4 +91,62 @@ RSpec.describe 'Create notes on issues', :js do expect(page).to have_selector '.gfm-project_member.current-user', text: user.username end + + shared_examples "when reference belongs to a private project" do + let(:project) { create(:project, :private, :repository) } + let(:issue) { create(:issue, project: project) } + + before do + sign_in(user) + end + + context 'when the user does not have permission to see the reference' do + before do + project.add_guest(user) + end + + it 'does not show the user the reference' do + visit project_issue_path(project, issue) + + expect(page).not_to have_content('closed via') + end + end + + context 'when the user has permission to see the reference' do + before do + project.add_developer(user) + end + + it 'shows the user the reference' do + visit project_issue_path(project, issue) + + page.within('div#notes li.note .system-note-message') do + expect(page).to have_content('closed via') + expect(page.find('a')).to have_content(reference_content) + end + end + end + end + + context 'when the issue is closed via a merge request' do + it_behaves_like "when reference belongs to a private project" do + let(:reference) { create(:merge_request, source_project: project) } + let(:reference_content) { reference.to_reference } + + before do + create(:resource_state_event, issue: issue, state: :closed, created_at: '2020-02-05', source_merge_request: reference) + end + end + end + + context 'when the issue is closed via a commit' do + it_behaves_like "when reference belongs to a private project" do + let(:reference) { create(:commit, project: project) } + let(:reference_content) { reference.short_sha } + + before do + create(:resource_state_event, issue: issue, state: :closed, created_at: '2020-02-05', source_commit: reference.id) + end + end + end end diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb index 09d3ad15641..4083b2a9e99 100644 --- a/spec/features/issues/user_comments_on_issue_spec.rb +++ b/spec/features/issues/user_comments_on_issue_spec.rb @@ -49,7 +49,7 @@ RSpec.describe "User comments on issue", :js do add_note(comment) - expect(page.find('svg.mermaid')).to have_content html_content + expect(page.find('svg.mermaid')).not_to have_content html_content within('svg.mermaid') { expect(page).not_to have_selector('img') } end diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb index 7fcc8200b1c..fa5d9f1c3a3 100644 --- a/spec/features/projects/packages_spec.rb +++ b/spec/features/projects/packages_spec.rb @@ -50,6 +50,22 @@ RSpec.describe 'Packages' do expect(page).to have_content 'Package deleted successfully' expect(page).not_to have_content(package.name) end + + context 'with too many package files' do + let_it_be(:package_files) { create_list(:package_file, 3, package: package) } + + before do + stub_application_setting(max_package_files_for_package_destruction: 1) + end + + it 'returns an error' do + first('[title="Remove package"]').click + click_button('Delete package') + + expect(page).to have_content "It's not possible to delete a package with more than 1 file." + expect(page).to have_content(package.name) + end + end end it_behaves_like 'shared package sorting' do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 15ec11c256f..05070e3afdf 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -53,6 +53,17 @@ RSpec.describe 'Protected Branches', :js do sign_in(user) end + it 'allows to create a protected branch with name containing HTML tags' do + visit project_protected_branches_path(project) + set_defaults + set_protected_branch_name('foo<b>bar<\b>') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('foo<b>bar<\b>') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('foo<b>bar<\b>') + end + describe 'Delete protected branch' do before do create(:protected_branch, project: project, name: 'fix') diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index b0f8b803141..fab48cf3178 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -39,6 +39,12 @@ RSpec.describe UsersFinder do expect(users).to contain_exactly(blocked_user) end + it 'does not filter by private emails search' do + users = described_class.new(user, search: normal_user.email).execute + + expect(users).to be_empty + end + it 'filters by blocked users' do users = described_class.new(user, blocked: true).execute @@ -135,6 +141,12 @@ RSpec.describe UsersFinder do expect(users).to contain_exactly(normal_user) end + + it 'filters by private emails search' do + users = described_class.new(admin, search: normal_user.email).execute + + expect(users).to contain_exactly(normal_user) + end end end end diff --git a/spec/frontend/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js index 56c09cd731e..143ccb9b930 100644 --- a/spec/frontend/create_item_dropdown_spec.js +++ b/spec/frontend/create_item_dropdown_spec.js @@ -17,6 +17,11 @@ const DROPDOWN_ITEM_DATA = [ id: 'three', text: 'three', }, + { + title: '<b>four</b>title', + id: '<b>four</b>id', + text: '<b>four</b>text', + }, ]; describe('CreateItemDropdown', () => { @@ -63,6 +68,10 @@ describe('CreateItemDropdown', () => { const $itemEls = $wrapperEl.find('.js-dropdown-content a'); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + + DROPDOWN_ITEM_DATA.forEach((dataItem, i) => { + expect($($itemEls[i]).text()).toEqual(dataItem.text); + }); }); }); @@ -177,7 +186,7 @@ describe('CreateItemDropdown', () => { const $itemEls = $wrapperEl.find('.js-dropdown-content a'); expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); - expect($($itemEls[3]).text()).toEqual('new-item-text'); + expect($($itemEls[DROPDOWN_ITEM_DATA.length]).text()).toEqual('new-item-text'); expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title'); }); }); diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js index 803ac4a219d..70c7f56b62f 100644 --- a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js +++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js @@ -16,13 +16,20 @@ export default [ 'text/html table', { input: [ - '<table>\n', + '<style type="text/css">\n', + '\n', + 'body {\n', + ' background: red;\n', + '}\n', + '\n', + '</style>\n', + '<table data-myattr="XSS">\n', '<tr>\n', '<th>Header 1</th>\n', '<th>Header 2</th>\n', '</tr>\n', '<tr>\n', - '<td>row 1, cell 1</td>\n', + '<td style="background: red;">row 1, cell 1</td>\n', '<td>row 1, cell 2</td>\n', '</tr>\n', '<tr>\n', diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js index 0bea84693f6..d3ec4e9dba0 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js @@ -209,7 +209,7 @@ describe('PackagesApp', () => { await waitForPromises(); - findDeletePackage().vm.$emit('end'); + findDeletePackage().vm.$emit('success'); expect(window.location.replace).toHaveBeenCalledWith( 'projectListUrl?showSuccessDeleteAlert=true', @@ -223,7 +223,7 @@ describe('PackagesApp', () => { await waitForPromises(); - findDeletePackage().vm.$emit('end'); + findDeletePackage().vm.$emit('success'); expect(window.location.replace).toHaveBeenCalledWith( 'groupListUrl?showSuccessDeleteAlert=true', diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js index 5de30829fa5..0492edf42f2 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js @@ -99,12 +99,13 @@ describe('DeletePackage', () => { }); describe('on mutation success', () => { - it('emits end event', async () => { + it('emits end and success events', async () => { createComponent(); await clickOnButtonAndWait(eventPayload); expect(wrapper.emitted('end')).toEqual([[]]); + expect(wrapper.emitted('success')).toEqual([[]]); }); it('does not call createFlash', async () => { @@ -128,10 +129,10 @@ describe('DeletePackage', () => { }); describe.each` - errorType | mutationResolverResponse - ${'connectionError'} | ${jest.fn().mockRejectedValue()} - ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())} - `('on mutation $errorType', ({ mutationResolverResponse }) => { + errorType | mutationResolverResponse | errorMessage + ${'connectionError'} | ${jest.fn().mockRejectedValue()} | ${DeletePackage.i18n.errorMessage} + ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())} | ${packageDestroyMutationError().errors[0].message} + `('on mutation $errorType', ({ mutationResolverResponse, errorMessage }) => { beforeEach(() => { mutationResolver = mutationResolverResponse; }); @@ -150,7 +151,7 @@ describe('DeletePackage', () => { await clickOnButtonAndWait(eventPayload); expect(createFlash).toHaveBeenCalledWith({ - message: DeletePackage.i18n.errorMessage, + message: expect.stringContaining(errorMessage), type: 'warning', captureError: true, error: expect.any(Error), diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 4f205e861dd..23823f7f724 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -594,4 +594,45 @@ RSpec.describe GitlabSchema.types['Project'] do expect(cluster_agent.agent_tokens.size).to be(count) end end + + describe 'service_desk_address' do + let(:user) { create(:user) } + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + id + serviceDeskAddress + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + before do + allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?) { true } + allow(::Gitlab::ServiceDeskEmail).to receive(:address_for_key) { 'address-suffix@example.com' } + end + + context 'when a user can admin issues' do + let(:project) { create(:project, :public, :service_desk_enabled) } + + before do + project.add_reporter(user) + end + + it 'is present' do + expect(subject.dig('data', 'project', 'serviceDeskAddress')).to be_present + end + end + + context 'when a user can not admin issues' do + let(:project) { create(:project, :public, :service_desk_disabled) } + + it 'is empty' do + expect(subject.dig('data', 'project', 'serviceDeskAddress')).to be_blank + end + end + end end diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index e736943914b..2d326bd77a6 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -17,4 +17,14 @@ RSpec.describe Banzai::Filter::BlockquoteFenceFilter do it 'allows trailing whitespace on blockquote fence lines' do expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n") end + + context 'when incomplete blockquote fences with multiple blocks are present' do + it 'does not raise timeout error' do + test_string = ">>>#{"\n```\nfoo\n```" * 20}" + + expect do + Timeout.timeout(2.seconds) { filter(test_string) } + end.not_to raise_error + end + end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 0713475d59b..5b77290ce2e 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -279,6 +279,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do end context 'when allow_local_network is' do + let(:shared_address_space_ips) { ['100.64.0.0', '100.64.127.127', '100.64.255.255'] } + let(:local_ips) do [ '192.168.1.2', @@ -292,7 +294,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do '[::ffff:ac10:20]', '[feef::1]', '[fee2::]', - '[fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa]' + '[fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa]', + *shared_address_space_ips ] end @@ -385,18 +388,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do '127.0.0.1', '127.0.0.2', '192.168.1.1', - '192.168.1.2', - '0:0:0:0:0:ffff:192.168.1.2', - '::ffff:c0a8:102', - '10.0.0.2', - '0:0:0:0:0:ffff:10.0.0.2', - '::ffff:a00:2', - '172.16.0.2', - '0:0:0:0:0:ffff:172.16.0.2', - '::ffff:ac10:20', - 'feef::1', - 'fee2::', - 'fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa', + *local_ips, '0:0:0:0:0:ffff:169.254.169.254', '::ffff:a9fe:a9fe', '::ffff:169.254.168.100', diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 8ad83da61f3..6471c3f89c7 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -80,6 +80,9 @@ RSpec.describe ApplicationSetting do it { is_expected.to validate_numericality_of(:dependency_proxy_ttl_group_policy_worker_capacity).only_integer.is_greater_than_or_equal_to(0) } it { is_expected.not_to allow_value(nil).for(:dependency_proxy_ttl_group_policy_worker_capacity) } + it { is_expected.to validate_numericality_of(:max_package_files_for_package_destruction).only_integer.is_greater_than(0) } + it { is_expected.not_to allow_value(nil).for(:max_package_files_for_package_destruction) } + it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) } it { is_expected.to validate_presence_of(:max_artifacts_size) } diff --git a/spec/models/concerns/integrations/enable_ssl_verification_spec.rb b/spec/models/concerns/integrations/enable_ssl_verification_spec.rb new file mode 100644 index 00000000000..802e950c0c2 --- /dev/null +++ b/spec/models/concerns/integrations/enable_ssl_verification_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::EnableSslVerification do + let(:described_class) do + Class.new(Integration) do + prepend Integrations::EnableSslVerification + + def fields + [ + { name: 'main_url' }, + { name: 'other_url' }, + { name: 'username' } + ] + end + end + end + + let(:integration) { described_class.new } + + include_context Integrations::EnableSslVerification +end diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb index 60ff6685c3d..b5684d153f2 100644 --- a/spec/models/integrations/bamboo_spec.rb +++ b/spec/models/integrations/bamboo_spec.rb @@ -23,6 +23,8 @@ RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do ) end + include_context Integrations::EnableSslVerification + describe 'Validations' do context 'when active' do before do diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb index 062e23d628e..dd64dcfc52c 100644 --- a/spec/models/integrations/drone_ci_spec.rb +++ b/spec/models/integrations/drone_ci_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do include ReactiveCachingHelpers + subject(:integration) { described_class.new } + describe 'validations' do context 'active' do before do @@ -59,6 +61,52 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do end end + include_context Integrations::EnableSslVerification do + describe '#enable_ssl_verification' do + before do + allow(integration).to receive(:new_record?).and_return(false) + end + + it 'returns true for a known hostname' do + integration.drone_url = 'https://cloud.drone.io' + + expect(integration.enable_ssl_verification).to be(true) + end + + it 'returns true for new records' do + allow(integration).to receive(:new_record?).and_return(true) + integration.drone_url = 'http://example.com' + + expect(integration.enable_ssl_verification).to be(true) + end + + it 'returns false for an unknown hostname' do + integration.drone_url = 'https://example.com' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns false for a HTTP URL' do + integration.drone_url = 'http://cloud.drone.io' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns false for an invalid URL' do + integration.drone_url = 'https://example.com:foo' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns the persisted value if present' do + integration.drone_url = 'https://cloud.drone.io' + integration.enable_ssl_verification = false + + expect(integration.enable_ssl_verification).to be(false) + end + end + end + it_behaves_like Integrations::HasWebHook do include_context :drone_ci_integration diff --git a/spec/models/integrations/irker_spec.rb b/spec/models/integrations/irker_spec.rb index 8b207e8b43e..8aea2c26dc5 100644 --- a/spec/models/integrations/irker_spec.rb +++ b/spec/models/integrations/irker_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' require 'socket' +require 'timeout' require 'json' RSpec.describe Integrations::Irker do @@ -37,6 +38,7 @@ RSpec.describe Integrations::Irker do before do @irker_server = TCPServer.new 'localhost', 0 + allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(true) allow(irker).to receive_messages( active: true, project: project, @@ -58,13 +60,17 @@ RSpec.describe Integrations::Irker do irker.execute(sample_data) conn = @irker_server.accept - conn.each_line do |line| - msg = Gitlab::Json.parse(line.chomp("\n")) - expect(msg.keys).to match_array(%w(to privmsg)) - expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits", - "irc://test.net/#test"]) + + Timeout.timeout(5) do + conn.each_line do |line| + msg = Gitlab::Json.parse(line.chomp("\n")) + expect(msg.keys).to match_array(%w(to privmsg)) + expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits", + "irc://test.net/#test"]) + end end - conn.close + ensure + conn.close if conn end end end diff --git a/spec/models/integrations/jenkins_spec.rb b/spec/models/integrations/jenkins_spec.rb index 9286d026290..3d6393f2793 100644 --- a/spec/models/integrations/jenkins_spec.rb +++ b/spec/models/integrations/jenkins_spec.rb @@ -24,6 +24,10 @@ RSpec.describe Integrations::Jenkins do let(:jenkins_authorization) { "Basic " + ::Base64.strict_encode64(jenkins_username + ':' + jenkins_password) } + include_context Integrations::EnableSslVerification do + let(:integration) { described_class.new(jenkins_params) } + end + it_behaves_like Integrations::HasWebHook do let(:integration) { described_class.new(jenkins_params) } let(:hook_url) { "http://#{ERB::Util.url_encode jenkins_username}:#{ERB::Util.url_encode jenkins_password}@jenkins.example.com/project/my_project" } diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index 1d81668f97d..c992cca10e8 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -130,6 +130,23 @@ RSpec.describe Integrations::Jira do end end + describe '.valid_jira_cloud_url?' do + using RSpec::Parameterized::TableSyntax + + where(:url, :result) do + 'https://abc.atlassian.net' | true + 'abc.atlassian.net' | false # This is how it behaves currently, but we may need to consider adding scheme if missing + 'https://somethingelse.com' | false + nil | false + end + + with_them do + specify do + expect(described_class.valid_jira_cloud_url?(url)).to eq(result) + end + end + end + describe '#create' do let(:params) do { diff --git a/spec/models/integrations/mock_ci_spec.rb b/spec/models/integrations/mock_ci_spec.rb new file mode 100644 index 00000000000..d29c63b3a97 --- /dev/null +++ b/spec/models/integrations/mock_ci_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::MockCi do + let_it_be(:project) { build(:project) } + + subject(:integration) { described_class.new(project: project, mock_service_url: generate(:url)) } + + include_context Integrations::EnableSslVerification + + describe '#commit_status' do + let(:sha) { generate(:sha) } + + def stub_request(*args) + WebMock.stub_request(:get, integration.commit_status_path(sha)).to_return(*args) + end + + def commit_status + integration.commit_status(sha, 'master') + end + + it 'returns allowed states' do + described_class::ALLOWED_STATES.each do |state| + stub_request(status: 200, body: { status: state }.to_json) + + expect(commit_status).to eq(state) + end + end + + it 'returns :pending for 404 responses' do + stub_request(status: 404) + + expect(commit_status).to eq(:pending) + end + + it 'returns :error for responses other than 200 or 404' do + stub_request(status: 500) + + expect(commit_status).to eq(:error) + end + + it 'returns :error for unknown states' do + stub_request(status: 200, body: { status: 'unknown' }.to_json) + + expect(commit_status).to eq(:error) + end + + it 'returns :error for invalid JSON' do + stub_request(status: 200, body: '') + + expect(commit_status).to eq(:error) + end + + it 'returns :error for non-hash JSON responses' do + stub_request(status: 200, body: 23.to_json) + + expect(commit_status).to eq(:error) + end + + it 'returns :error for JSON responses without a status' do + stub_request(status: 200, body: { foo: :bar }.to_json) + + expect(commit_status).to eq(:error) + end + + it 'returns :error when connection is refused' do + stub_request(status: 500).to_raise(Errno::ECONNREFUSED) + + expect(commit_status).to eq(:error) + end + end +end diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb index 0713141ea08..e1f4e577503 100644 --- a/spec/models/integrations/teamcity_spec.rb +++ b/spec/models/integrations/teamcity_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do include ReactiveCachingHelpers include StubRequests - let(:teamcity_url) { 'http://gitlab.com/teamcity' } - let(:teamcity_full_url) { 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' } + let(:teamcity_url) { 'https://gitlab.teamcity.com' } + let(:teamcity_full_url) { 'https://gitlab.teamcity.com/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' } let(:project) { create(:project) } subject(:integration) do @@ -22,6 +22,52 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do ) end + include_context Integrations::EnableSslVerification do + describe '#enable_ssl_verification' do + before do + allow(integration).to receive(:new_record?).and_return(false) + end + + it 'returns true for a known hostname' do + integration.teamcity_url = 'https://example.teamcity.com' + + expect(integration.enable_ssl_verification).to be(true) + end + + it 'returns true for new records' do + allow(integration).to receive(:new_record?).and_return(true) + integration.teamcity_url = 'http://example.com' + + expect(integration.enable_ssl_verification).to be(true) + end + + it 'returns false for an unknown hostname' do + integration.teamcity_url = 'https://sub.example.teamcity.com' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns false for a HTTP URL' do + integration.teamcity_url = 'http://example.teamcity.com' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns false for an invalid URL' do + integration.teamcity_url = 'https://example.com:foo' + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'returns the persisted value if present' do + integration.teamcity_url = 'https://example.teamcity.com' + integration.enable_ssl_verification = false + + expect(integration.enable_ssl_verification).to be(false) + end + end + end + describe 'Validations' do context 'when integration is active' do before do @@ -140,22 +186,22 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do it 'returns a specific URL when status is 500' do stub_request(status: 500) - is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + is_expected.to eq("#{teamcity_url}/viewLog.html?buildTypeId=foo") end it 'returns a build URL when teamcity_url has no trailing slash' do stub_request(body: %q({"build":{"id":"666"}})) - is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + is_expected.to eq("#{teamcity_url}/viewLog.html?buildId=666&buildTypeId=foo") end context 'teamcity_url has trailing slash' do - let(:teamcity_url) { 'http://gitlab.com/teamcity/' } + let(:teamcity_url) { 'https://gitlab.teamcity.com/' } it 'returns a build URL' do stub_request(body: %q({"build":{"id":"666"}})) - is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + is_expected.to eq('https://gitlab.teamcity.com/viewLog.html?buildId=666&buildTypeId=foo') end end @@ -299,7 +345,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do end def stub_post_to_build_queue(branch:) - teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/buildQueue' + teamcity_full_url = "#{teamcity_url}/httpAuth/app/rest/buildQueue" body ||= %Q(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>) auth = %w(mic password) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b5d4614d206..7423e2bd4f3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2267,6 +2267,12 @@ RSpec.describe User do describe '.search' do let_it_be(:user) { create(:user, name: 'user', username: 'usern', email: 'email@example.com') } + let_it_be(:public_email) do + create(:email, :confirmed, user: user, email: 'publicemail@example.com').tap do |email| + user.update!(public_email: email.email) + end + end + let_it_be(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@example.com') } let_it_be(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@example.com') } let_it_be(:email) { create(:email, user: user, email: 'alias@example.com') } @@ -2294,30 +2300,31 @@ RSpec.describe User do end describe 'email matching' do - it 'returns users with a matching Email' do - expect(described_class.search(user.email)).to eq([user]) + it 'returns users with a matching public email' do + expect(described_class.search(user.public_email)).to match_array([user]) end - it 'does not return users with a partially matching Email' do - expect(described_class.search(user.email[1...-1])).to be_empty + it 'does not return users with a partially matching public email' do + expect(described_class.search(user.public_email[1...-1])).to be_empty end - it 'returns users with a matching Email regardless of the casing' do - expect(described_class.search(user2.email.upcase)).to eq([user2]) + it 'returns users with a matching public email regardless of the casing' do + expect(described_class.search(user.public_email.upcase)).to match_array([user]) end - end - describe 'secondary email matching' do - it 'returns users with a matching secondary email' do - expect(described_class.search(email.email)).to include(email.user) + it 'does not return users with a matching private email' do + expect(described_class.search(user.email)).to be_empty + expect(described_class.search(email.email)).to be_empty end - it 'does not return users with a matching part of secondary email' do - expect(described_class.search(email.email[1...-1])).to be_empty - end + context 'with private emails search' do + it 'returns users with matching private email' do + expect(described_class.search(user.email, with_private_emails: true)).to match_array([user]) + end - it 'returns users with a matching secondary email regardless of the casing' do - expect(described_class.search(email.email.upcase)).to include(email.user) + it 'returns users with matching private secondary email' do + expect(described_class.search(email.email, with_private_emails: true)).to match_array([user]) + end end end @@ -2418,6 +2425,80 @@ RSpec.describe User do end end + describe '.search_with_public_emails' do + let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) } + let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) } + let_it_be(:public_email) do + create(:email, :confirmed, user: another_user, email: 'alias@example.com').tap do |email| + another_user.update!(public_email: email.email) + end + end + + let_it_be(:secondary_email) do + create(:email, :confirmed, user: another_user, email: 'secondary@example.com') + end + + it 'returns users with a matching name' do + expect(described_class.search_with_public_emails(user.name)).to match_array([user]) + end + + it 'returns users with a partially matching name' do + expect(described_class.search_with_public_emails(user.name[0..2])).to match_array([user]) + end + + it 'returns users with a matching name regardless of the casing' do + expect(described_class.search_with_public_emails(user.name.upcase)).to match_array([user]) + end + + it 'returns users with a matching public email' do + expect(described_class.search_with_public_emails(another_user.public_email)).to match_array([another_user]) + end + + it 'does not return users with a partially matching email' do + expect(described_class.search_with_public_emails(another_user.public_email[1...-1])).to be_empty + end + + it 'returns users with a matching email regardless of the casing' do + expect(described_class.search_with_public_emails(another_user.public_email.upcase)).to match_array([another_user]) + end + + it 'returns users with a matching username' do + expect(described_class.search_with_public_emails(user.username)).to match_array([user]) + end + + it 'returns users with a partially matching username' do + expect(described_class.search_with_public_emails(user.username[0..2])).to match_array([user]) + end + + it 'returns users with a matching username regardless of the casing' do + expect(described_class.search_with_public_emails(user.username.upcase)).to match_array([user]) + end + + it 'does not return users with a matching whole private email' do + expect(described_class.search_with_public_emails(user.email)).not_to include(user) + end + + it 'does not return users with a matching whole private email' do + expect(described_class.search_with_public_emails(secondary_email.email)).to be_empty + end + + it 'does not return users with a matching part of secondary email' do + expect(described_class.search_with_public_emails(secondary_email.email[1...-1])).to be_empty + end + + it 'does not return users with a matching part of private email' do + expect(described_class.search_with_public_emails(user.email[1...-1])).to be_empty + end + + it 'returns no matches for an empty string' do + expect(described_class.search_with_public_emails('')).to be_empty + end + + it 'returns no matches for nil' do + expect(described_class.search_with_public_emails(nil)).to be_empty + end + end + describe '.search_with_secondary_emails' do let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) } let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) } diff --git a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb index e5ced419ecf..bd44f32dbaa 100644 --- a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb @@ -88,6 +88,20 @@ RSpec.describe 'Destroying a package' do expect(mutation_response['errors']).to eq(['Failed to remove the package']) end + + context 'with too many files' do + let_it_be(:package_files) { create_list(:package_file, 3, package: package) } + + before do + stub_application_setting(max_package_files_for_package_destruction: 1) + end + + it 'returns the errors in the response' do + mutation_request + + expect(mutation_response['errors']).to match_array(["It's not possible to delete a package with more than 1 file."]) + end + end end end end diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 9b7538547f6..c10626ecc6c 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -355,6 +355,23 @@ RSpec.describe API::ProjectPackages do expect(response).to have_gitlab_http_status(:no_content) end + + context 'with too many files' do + let!(:package_files) { create_list(:package_file, 3, package: package1) } + + before do + stub_application_setting(max_package_files_for_package_destruction: 1) + end + + it 'returns 400' do + project.add_maintainer(user) + + delete api(package_url, user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include("It's not possible to delete a package with more than 1 file.") + end + end end context 'with a maven package' do diff --git a/spec/requests/jira_connect/users_controller_spec.rb b/spec/requests/jira_connect/users_controller_spec.rb new file mode 100644 index 00000000000..c648d28c1bc --- /dev/null +++ b/spec/requests/jira_connect/users_controller_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnect::UsersController do + describe 'GET /-/jira_connect/users' do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'with a valid host' do + let(:return_to) { 'https://testcompany.atlassian.net/plugins/servlet/ac/gitlab-jira-connect-staging.gitlab.com/gitlab-configuration' } + + it 'includes a return url' do + get '/-/jira_connect/users', params: { return_to: return_to } + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include('Return to GitLab') + end + end + + context 'with an invalid host' do + let(:return_to) { 'https://evil.com' } + + it 'does not include a return url' do + get '/-/jira_connect/users', params: { return_to: return_to } + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).not_to include('Return to GitLab') + end + end + end +end diff --git a/spec/services/packages/destroy_package_service_spec.rb b/spec/services/packages/destroy_package_service_spec.rb index 92db8da968c..a284f88a0fe 100644 --- a/spec/services/packages/destroy_package_service_spec.rb +++ b/spec/services/packages/destroy_package_service_spec.rb @@ -30,6 +30,24 @@ RSpec.describe Packages::DestroyPackageService do end end + context 'with too many package files' do + let!(:package_files) { create_list(:package_file, 2, package: package) } + + before do + stub_application_setting(max_package_files_for_package_destruction: 1) + end + + it 'returns an error ServiceResponse' do + response = service.execute + + expect(package).not_to receive(:sync_maven_metadata) + expect(response).to be_a(ServiceResponse) + expect(response).to be_error + expect(response.message).to eq("It's not possible to delete a package with more than 1 file.") + expect(response.status).to eq(:error) + end + end + context 'when the destroy is not successful' do before do allow(package).to receive(:destroy!).and_raise(StandardError, "test") diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 756c775be9b..707e87d2477 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -24,38 +24,14 @@ RSpec.describe ProtectedBranches::CreateService do expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) end - context 'when name has escaped HTML' do - let(:name) { 'feature->test' } + context 'when protecting a branch with a name that contains HTML tags' do + let(:name) { 'foo<b>bar<\b>' } - it 'creates the new protected branch matching the unescaped version' do - expect { service.execute }.to change(ProtectedBranch, :count).by(1) - expect(project.protected_branches.last.name).to eq('feature->test') - end - - context 'and name contains HTML tags' do - let(:name) { '<b>master</b>' } - - it 'creates the new protected branch with sanitized name' do - expect { service.execute }.to change(ProtectedBranch, :count).by(1) - expect(project.protected_branches.last.name).to eq('master') - end - - context 'and contains unsafe HTML' do - let(:name) { '<script>alert('foo');</script>' } + subject(:service) { described_class.new(project, user, params) } - it 'does not create the new protected branch' do - expect { service.execute }.not_to change(ProtectedBranch, :count) - end - end - end - - context 'when name contains unescaped HTML tags' do - let(:name) { '<b>master</b>' } - - it 'creates the new protected branch with sanitized name' do - expect { service.execute }.to change(ProtectedBranch, :count).by(1) - expect(project.protected_branches.last.name).to eq('master') - end + it 'creates a new protected branch' do + expect { service.execute }.to change(ProtectedBranch, :count).by(1) + expect(project.protected_branches.last.name).to eq(name) end end diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index b5cf1a54aff..441eea9d7dd 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -18,35 +18,14 @@ RSpec.describe ProtectedBranches::UpdateService do expect(result.reload.name).to eq(params[:name]) end - context 'when name has escaped HTML' do - let(:new_name) { 'feature->test' } + context 'when updating name of a protected branch to one that contains HTML tags' do + let(:new_name) { 'foo<b>bar<\b>' } + let(:result) { service.execute(protected_branch) } - it 'updates protected branch name with unescaped HTML' do - expect(result.reload.name).to eq('feature->test') - end - - context 'and name contains HTML tags' do - let(:new_name) { '<b>master</b>' } - - it 'updates protected branch name with sanitized name' do - expect(result.reload.name).to eq('master') - end - - context 'and contains unsafe HTML' do - let(:new_name) { '<script>alert('foo');</script>' } - - it 'does not update the protected branch' do - expect(result.reload.name).to eq(protected_branch.name) - end - end - end - end - - context 'when name contains unescaped HTML tags' do - let(:new_name) { '<b>master</b>' } + subject(:service) { described_class.new(project, user, params) } - it 'updates protected branch name with sanitized name' do - expect(result.reload.name).to eq('master') + it 'updates a protected branch' do + expect(result.reload.name).to eq(new_name) end end diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb index e85a43eb51c..5c02f839998 100644 --- a/spec/services/protected_tags/create_service_spec.rb +++ b/spec/services/protected_tags/create_service_spec.rb @@ -5,9 +5,10 @@ require 'spec_helper' RSpec.describe ProtectedTags::CreateService do let(:project) { create(:project) } let(:user) { project.owner } + let(:name) { 'master' } let(:params) do { - name: 'master', + name: name, create_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }] } end @@ -19,5 +20,16 @@ RSpec.describe ProtectedTags::CreateService do expect { service.execute }.to change(ProtectedTag, :count).by(1) expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) end + + context 'protecting a tag with a name that contains HTML tags' do + let(:name) { 'foo<b>bar<\b>' } + + subject(:service) { described_class.new(project, user, params) } + + it 'creates a new protected tag' do + expect { service.execute }.to change(ProtectedTag, :count).by(1) + expect(project.protected_tags.last.name).to eq(name) + end + end end end diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb index ed151ca2347..cccfabdd040 100644 --- a/spec/services/protected_tags/update_service_spec.rb +++ b/spec/services/protected_tags/update_service_spec.rb @@ -6,7 +6,8 @@ RSpec.describe ProtectedTags::UpdateService do let(:protected_tag) { create(:protected_tag) } let(:project) { protected_tag.project } let(:user) { project.owner } - let(:params) { { name: 'new protected tag name' } } + let(:new_name) {'new protected tag name' } + let(:params) { { name: new_name } } describe '#execute' do subject(:service) { described_class.new(project, user, params) } @@ -17,6 +18,17 @@ RSpec.describe ProtectedTags::UpdateService do expect(result.reload.name).to eq(params[:name]) end + context 'when updating protected tag with a name that contains HTML tags' do + let(:new_name) { 'foo<b>bar<\b>' } + let(:result) { service.execute(protected_tag) } + + subject(:service) { described_class.new(project, user, params) } + + it 'updates a protected tag' do + expect(result.reload.name).to eq(new_name) + end + end + context 'without admin_project permissions' do let(:user) { create(:user) } diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb index ba32ccbb6f1..b941e7c4808 100644 --- a/spec/support/helpers/dns_helpers.rb +++ b/spec/support/helpers/dns_helpers.rb @@ -23,7 +23,15 @@ module DnsHelpers end def permit_local_dns! - local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i + local_addresses = %r{ + \A + ::1? | # IPV6 + (127|10)\.0\.0\.\d{1,3} | # 127.0.0.x or 10.0.0.x local network + (192\.168|172\.16)\.\d{1,3}\.\d{1,3} | # 192.168.x.x or 172.16.x.x local network + 0\.0\.0\.0 | # loopback + localhost + \z + }xi allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original end diff --git a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb new file mode 100644 index 00000000000..c698e06c2a2 --- /dev/null +++ b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_context Integrations::EnableSslVerification do + # This is added to the global setup, to make sure all calls to + # `Gitlab::HTTP` in the main model spec are passing the `verify:` option. + before do + allow(Gitlab::HTTP).to receive(:perform_request) + .with(anything, anything, include(verify: true)) + .and_call_original + end + + describe 'accessors' do + it { is_expected.to respond_to(:enable_ssl_verification) } + it { is_expected.to respond_to(:enable_ssl_verification?) } + end + + describe '#initialize_properties' do + it 'enables the setting by default' do + expect(integration.enable_ssl_verification).to be(true) + end + + it 'does not enable the setting if the record is already persisted' do + allow(integration).to receive(:new_record?).and_return(false) + + integration.enable_ssl_verification = false + integration.send(:initialize_properties) + + expect(integration.enable_ssl_verification).to be(false) + end + + it 'does not enable the setting if a custom value was set' do + integration = described_class.new(enable_ssl_verification: false) + + expect(integration.enable_ssl_verification).to be(false) + end + end + + describe '#fields' do + it 'inserts the checkbox field after the first URL field, or at the end' do + names = integration.fields.pluck(:name) + url_index = names.index { |name| name.ends_with?('_url') } + insert_index = url_index ? url_index + 1 : names.size - 1 + + expect(names.index('enable_ssl_verification')).to eq insert_index + end + end +end diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb index 1fa340a0cf4..ae72cb6ec5d 100644 --- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -37,6 +37,16 @@ RSpec.shared_examples Integrations::HasWebHook do it 'returns a boolean' do expect(integration.hook_ssl_verification).to be_in([true, false]) end + + it 'delegates to #enable_ssl_verification if the concern is included' do + next unless integration.is_a?(Integrations::EnableSslVerification) + + [true, false].each do |value| + integration.enable_ssl_verification = value + + expect(integration.hook_ssl_verification).to be(value) + end + end end describe '#update_web_hook!' do diff --git a/spec/workers/irker_worker_spec.rb b/spec/workers/irker_worker_spec.rb index aa1f1d2fe1d..c3d40ad2783 100644 --- a/spec/workers/irker_worker_spec.rb +++ b/spec/workers/irker_worker_spec.rb @@ -21,7 +21,7 @@ RSpec.describe IrkerWorker, '#perform' do channels, false, push_data, - server_settings + HashWithIndifferentAccess.new(server_settings) ] end @@ -35,6 +35,14 @@ RSpec.describe IrkerWorker, '#perform' do allow(tcp_socket).to receive(:close).and_return(true) end + context 'local requests are not allowed' do + before do + allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(false) + end + + it { expect(worker.perform(*arguments)).to be_falsey } + end + context 'connection fails' do before do allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED.new('test')) @@ -44,6 +52,11 @@ RSpec.describe IrkerWorker, '#perform' do end context 'connection successful' do + before do + allow(Gitlab::CurrentSettings) + .to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(true) + end + it { expect(subject.perform(*arguments)).to be_truthy } context 'new branch' do diff --git a/yarn.lock b/yarn.lock index 4e493812ead..2b5e209c495 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4918,11 +4918,16 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -dompurify@2.3.3, dompurify@^2.3.3: +dompurify@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== +dompurify@2.3.4, dompurify@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6" + integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ== + domutils@^1.5.1: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -8440,16 +8445,16 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^8.13.4: - version "8.13.4" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.13.4.tgz#924cb85f39380285e0a99f245c66cfa61014a2e1" - integrity sha512-zdWtsXabVy1PEAE25Jkm4zbTDlQe8rqNlTMq2B3j+D+NxDskJEY5OsgalarvNLsw+b5xFa1a8D1xcm/PijrDow== +mermaid@^8.13.10: + version "8.13.10" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.13.10.tgz#b9d733b178bbf7416b9b46e39d566c7c28b75688" + integrity sha512-2ANep359uML87+wiYaWSu83eg9Qc0xCLnNJdCh100m4v0orS3fp8SScsZLcDSElRGHi+1zuVJsEEVEWH05+COQ== dependencies: "@braintree/sanitize-url" "^3.1.0" d3 "^7.0.0" dagre "^0.8.5" dagre-d3 "^0.6.4" - dompurify "2.3.3" + dompurify "2.3.4" graphlib "^2.1.8" khroma "^1.4.1" moment-mini "^2.24.0" |