Update upstream source from tag 'upstream/15.10.8+ds1'

Update to upstream version '15.10.8+ds1'
with Debian dir d1899e4103
This commit is contained in:
Mohammed Bilal 2023-06-09 08:13:14 +05:30
commit f87c4d536a
65 changed files with 1635 additions and 632 deletions

View file

@ -1144,6 +1144,8 @@
rules: rules:
- <<: *if-not-canonical-namespace - <<: *if-not-canonical-namespace
when: never when: never
- <<: *if-security-merge-request
when: never
- <<: *if-merge-request-targeting-stable-branch - <<: *if-merge-request-targeting-stable-branch
when: always when: always

View file

@ -2,6 +2,30 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 15.10.8 (2023-06-05)
### Fixed (1 change)
- [Convert some regex to use Gitlab::UntrustedRegexp](gitlab-org/security/gitlab@251e0f30177cf458f4384662bdfc14d404c5b98d)
### Security (15 changes)
- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@5893c3c3311052744175051c8393e451771ea100) ([merge request](gitlab-org/security/gitlab!3201))
- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@da5ecc94a6db6d3e2180d7bd7e2b32e903f7f5c6) ([merge request](gitlab-org/security/gitlab!3291))
- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@9995ef153a96621da0d0f2469734dd895485a4d7) ([merge request](gitlab-org/security/gitlab!3284))
- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@cf73c05b31cf466011fbb3492495a7acbcd78d5f) ([merge request](gitlab-org/security/gitlab!3276))
- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@eb4e906ecd8d56ef71c97ab74a32c06c0a9bd7b6) ([merge request](gitlab-org/security/gitlab!3263))
- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@d10133feff8201b45c8a4c29681db4f167e23d59) ([merge request](gitlab-org/security/gitlab!3280))
- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@ca0f866a5663af8ffa094b0ffd152e5031beecd5) ([merge request](gitlab-org/security/gitlab!3224))
- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@f66129126262d000c77f36ea2b1b0f5e88f1be13) ([merge request](gitlab-org/security/gitlab!3256))
- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@7fb2dfc1135d74ea261e633ec0a828fa8a8c7ef0) ([merge request](gitlab-org/security/gitlab!3248))
- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@2a50fd1fd3c4610871644237edc22bbdc9cbcb1d) ([merge request](gitlab-org/security/gitlab!3255))
- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@2e969309ad7b3fff551857ee481a154cb3be73f4) ([merge request](gitlab-org/security/gitlab!3268))
- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@7ec6ab8c11d3732b53c7adc951d3da9972695bff) ([merge request](gitlab-org/security/gitlab!3287))
- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@2a2035520eab7263d157b312f5fb7d3d82440ccf) ([merge request](gitlab-org/security/gitlab!3250))
- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@457cd1086688b1a44f1f771c407e8d1eaa8f2951) ([merge request](gitlab-org/security/gitlab!3231))
- [Validate description length in labels](gitlab-org/security/gitlab@c6f95221685f4475a8b91190c61ee4208e257844) ([merge request](gitlab-org/security/gitlab!3243))
## 15.10.7 (2023-05-10) ## 15.10.7 (2023-05-10)
### Fixed (1 change) ### Fixed (1 change)

View file

@ -1 +1 @@
15.10.7 15.10.8

View file

@ -1 +1 @@
15.10.7 15.10.8

View file

@ -1 +1 @@
15.10.7 15.10.8

View file

@ -26,7 +26,9 @@ export default class SingleFileDiff {
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
this.$chevronRightIcon = $('.diff-toggle-caret .chevron-right', this.file); this.$chevronRightIcon = $('.diff-toggle-caret .chevron-right', this.file);
this.$chevronDownIcon = $('.diff-toggle-caret .chevron-down', this.file); this.$chevronDownIcon = $('.diff-toggle-caret .chevron-down', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diffForPath'); this.diffForPath = this.content
.find('div:not(.note-text)[data-diff-for-path]')
.data('diffForPath');
this.isOpen = !this.diffForPath; this.isOpen = !this.diffForPath;
if (this.diffForPath) { if (this.diffForPath) {
this.collapsedContent = this.content; this.collapsedContent = this.content;

View file

@ -1,17 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class AbuseReportsController < ApplicationController class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new, :add_category] before_action :set_user, only: [:add_category]
feature_category :insider_threat feature_category :insider_threat
def new
@abuse_report = AbuseReport.new(
user_id: @user.id,
reported_from_url: params.fetch(:ref_url, '')
)
end
def add_category def add_category
@abuse_report = AbuseReport.new( @abuse_report = AbuseReport.new(
user_id: @user.id, user_id: @user.id,

View file

@ -9,6 +9,7 @@ module Ci
STORE_COLUMN = :file_store STORE_COLUMN = :file_store
NotSupportedAdapterError = Class.new(StandardError) NotSupportedAdapterError = Class.new(StandardError)
FILE_FORMAT_ADAPTERS = { FILE_FORMAT_ADAPTERS = {
# While zip is a streamable file format, performing streaming # While zip is a streamable file format, performing streaming
# reads requires that each entry in the zip has certain headers # reads requires that each entry in the zip has certain headers
@ -41,6 +42,9 @@ module Ci
raise NotSupportedAdapterError, 'This file format requires a dedicated adapter' raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
end end
::Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator
.new(file: file, file_format: file_format.to_sym).validate!
log_artifacts_filesize(file.model) log_artifacts_filesize(file.model)
file.open do |stream| file.open do |stream|

View file

@ -3,19 +3,6 @@
module Exportable module Exportable
extend ActiveSupport::Concern extend ActiveSupport::Concern
def readable_records(association, current_user: nil)
association_records = try(association)
return unless association_records.present?
if has_many_association?(association)
DeclarativePolicy.user_scope do
association_records.select { |record| readable_record?(record, current_user) }
end
else
readable_record?(association_records, current_user) ? association_records : nil
end
end
def exportable_association?(association, current_user: nil) def exportable_association?(association, current_user: nil)
return false unless respond_to?(association) return false unless respond_to?(association)
return true if has_many_association?(association) return true if has_many_association?(association)
@ -30,8 +17,17 @@ module Exportable
exportable_restricted_associations & keys exportable_restricted_associations & keys
end end
def has_many_association?(association_name) def to_authorized_json(keys_to_authorize, current_user, options)
self.class.reflect_on_association(association_name)&.macro == :has_many modified_options = filtered_associations_opts(options, keys_to_authorize)
record_hash = as_json(modified_options).with_indifferent_access
keys_to_authorize.each do |key|
next unless record_hash.key?(key)
record_hash[key] = authorized_association_records(key, current_user, options)
end
record_hash.to_json
end end
private private
@ -47,4 +43,47 @@ module Exportable
record.readable_by?(user) record.readable_by?(user)
end end
end end
def has_many_association?(association_name)
self.class.reflect_on_association(association_name)&.macro == :has_many
end
def readable_records(association, current_user: nil)
association_records = try(association)
return unless association_records.present?
if has_many_association?(association)
DeclarativePolicy.user_scope do
association_records.select { |record| readable_record?(record, current_user) }
end
else
readable_record?(association_records, current_user) ? association_records : nil
end
end
def authorized_association_records(key, current_user, options)
records = readable_records(key, current_user: current_user)
empty_assoc = has_many_association?(key) ? [] : nil
return empty_assoc unless records.present?
assoc_opts = association_options(key, options)&.dig(key)
records.as_json(assoc_opts)
end
def filtered_associations_opts(options, associations)
options_copy = options.deep_dup
associations.each do |key|
assoc_opts = association_options(key, options_copy)
next unless assoc_opts
assoc_opts[key] = { only: [:id] }
end
options_copy
end
def association_options(key, options)
options[:include].find { |assoc| assoc.key?(key) }
end
end end

View file

@ -27,6 +27,7 @@ module Issuable
include ClosedAtFilterable include ClosedAtFilterable
include VersionedDescription include VersionedDescription
include SortableTitle include SortableTitle
include Exportable
TITLE_LENGTH_MAX = 255 TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800 TITLE_HTML_LENGTH_MAX = 800
@ -226,6 +227,10 @@ module Issuable
issuable_severity&.severity || IssuableSeverity::DEFAULT issuable_severity&.severity || IssuableSeverity::DEFAULT
end end
def exportable_restricted_associations
super + [:notes]
end
private private
def validate_description_length? def validate_description_length?

View file

@ -25,7 +25,6 @@ class Issue < ApplicationRecord
include FromUnion include FromUnion
include EachBatch include EachBatch
include PgFullTextSearchable include PgFullTextSearchable
include Exportable
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override

View file

@ -13,6 +13,7 @@ class Label < ApplicationRecord
cache_markdown_field :description, pipeline: :single_line cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc') DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
DESCRIPTION_LENGTH_MAX = 512.kilobytes
attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR
@ -31,6 +32,10 @@ class Label < ApplicationRecord
validates :title, uniqueness: { scope: [:group_id, :project_id] } validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 } validates :title, length: { maximum: 255 }
# we validate the description against DESCRIPTION_LENGTH_MAX only for labels being created and on updates if
# the description changes to avoid breaking the existing labels which may have their descriptions longer
validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :validate_description_length?
default_scope { order(title: :asc) } # rubocop:disable Cop/DefaultScope default_scope { order(title: :asc) } # rubocop:disable Cop/DefaultScope
scope :templates, -> { where(template: true, type: [Label.name, nil]) } scope :templates, -> { where(template: true, type: [Label.name, nil]) }
@ -277,6 +282,16 @@ class Label < ApplicationRecord
private private
def validate_description_length?
return false unless description_changed?
previous_description = changes['description'].first
# previous_description will be nil for new records
return true if previous_description.blank?
previous_description.bytesize <= DESCRIPTION_LENGTH_MAX || description.bytesize > previous_description.bytesize
end
def issues_count(user, params = {}) def issues_count(user, params = {})
params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
IssuesFinder.new(user, params.with_indifferent_access).execute.count IssuesFinder.new(user, params.with_indifferent_access).execute.count

View file

@ -117,12 +117,14 @@ class ProjectTeam
owners.include?(user) owners.include?(user)
end end
def import(source_project, current_user = nil) def import(source_project, current_user)
target_project = project target_project = project
source_members = source_project.project_members.to_a source_members = source_project.project_members.to_a
target_user_ids = target_project.project_members.pluck(:user_id) target_user_ids = target_project.project_members.pluck(:user_id)
importer_access_level = max_member_access(current_user.id)
source_members.reject! do |member| source_members.reject! do |member|
# Skip if user already present in team # Skip if user already present in team
!member.invite? && target_user_ids.include?(member.user_id) !member.invite? && target_user_ids.include?(member.user_id)
@ -132,6 +134,8 @@ class ProjectTeam
new_member = member.dup new_member = member.dup
new_member.id = nil new_member.id = nil
new_member.source = target_project new_member.source = target_project
# So that a maintainer cannot import a member with owner access
new_member.access_level = [new_member.access_level, importer_access_level].min
new_member.created_by = current_user new_member.created_by = current_user
new_member new_member
end end

View file

@ -1524,7 +1524,9 @@ class User < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
def primary_email_verified? def primary_email_verified?
confirmed? && !temp_oauth_email? return false unless confirmed? && !temp_oauth_email?
!email_changed? || new_record?
end end
def accept_pending_invitations! def accept_pending_invitations!

View file

@ -206,7 +206,7 @@ InitializerConnections.raise_if_new_database_connection do
end end
# Spam reports # Spam reports
resources :abuse_reports, only: [:new, :create] do resources :abuse_reports, only: [:create] do
collection do collection do
post :add_category post :add_category
end end

View file

@ -15116,7 +15116,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
Represents vulnerable project counts for each grade. Represents vulnerable project counts for each grade.
Returns [`[VulnerableProjectsByGrade!]!`](#vulnerableprojectsbygrade). Returns [`[VulnerableProjectsByGrade!]`](#vulnerableprojectsbygrade).
###### Arguments ###### Arguments

View file

@ -2368,6 +2368,11 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Import members from another project. Import members from another project.
If the importing member's role in the target project is:
- Maintainer, then members with the Owner role in the source project are imported with the Maintainer role.
- Owner, then members with the Owner role in the source project are imported with the Owner role.
```plaintext ```plaintext
POST /projects/:id/import_project_members/:project_id POST /projects/:id/import_project_members/:project_id
``` ```

View file

@ -200,6 +200,11 @@ Prerequisite:
- You must have the Maintainer or Owner role. - You must have the Maintainer or Owner role.
If the importing member's role in the target project is:
- Maintainer, then members with the Owner role in the source project are imported with the Maintainer role.
- Owner, then members with the Owner role in the source project are imported with the Owner role.
To import users: To import users:
1. On the top bar, select **Main menu > Projects** and find your project. 1. On the top bar, select **Main menu > Projects** and find your project.

View file

@ -27,6 +27,11 @@ module API
end end
helpers do helpers do
params :package_name do
requires :package_name, type: String, file_path: true, desc: 'Package name',
documentation: { example: 'mypackage' }
end
def redirect_or_present_audit_report def redirect_or_present_audit_report
redirect_registry_request( redirect_registry_request(
forward_to_registry: true, forward_to_registry: true,
@ -161,7 +166,7 @@ module API
tags %w[npm_packages] tags %w[npm_packages]
end end
params do params do
requires :package_name, type: String, desc: 'Package name' use :package_name
end end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do

View file

@ -17,19 +17,21 @@ module Banzai
# encoded and will therefore not interfere with the detection of the dollar syntax. # encoded and will therefore not interfere with the detection of the dollar syntax.
# Corresponds to the "$...$" syntax # Corresponds to the "$...$" syntax
DOLLAR_INLINE_PATTERN = %r{ DOLLAR_INLINE_UNTRUSTED =
(?<matched>\$(?<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$) '(?P<matched>\$(?P<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)'
}x.freeze DOLLAR_INLINE_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false)
# Corresponds to the "$$...$$" syntax # Corresponds to the "$$...$$" syntax
DOLLAR_DISPLAY_INLINE_PATTERN = %r{ DOLLAR_DISPLAY_INLINE_UNTRUSTED =
(?<matched>\$\$\ *(?<math>[^$\n]+?)\ *\$\$) '(?P<matched>\$\$\ *(?P<math>[^$\n]+?)\ *\$\$)'
}x.freeze DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false)
# Order dependent. Handle the `$$` syntax before the `$` syntax # Order dependent. Handle the `$$` syntax before the `$` syntax
DOLLAR_MATH_PIPELINE = [ DOLLAR_MATH_PIPELINE = [
{ pattern: DOLLAR_DISPLAY_INLINE_PATTERN, style: :display }, { pattern: DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX, style: :display },
{ pattern: DOLLAR_INLINE_PATTERN, style: :inline } { pattern: DOLLAR_INLINE_UNTRUSTED_REGEX, style: :inline }
].freeze ].freeze
# Do not recognize math inside these tags # Do not recognize math inside these tags
@ -46,16 +48,18 @@ module Banzai
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
node_html = node.to_html node_html = node.to_html
next unless node_html.match?(DOLLAR_INLINE_PATTERN) || next unless DOLLAR_INLINE_UNTRUSTED_REGEX.match?(node_html) ||
node_html.match?(DOLLAR_DISPLAY_INLINE_PATTERN) DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX.match?(node_html)
temp_doc = Nokogiri::HTML.fragment(node_html) temp_doc = Nokogiri::HTML.fragment(node_html)
DOLLAR_MATH_PIPELINE.each do |pipeline| DOLLAR_MATH_PIPELINE.each do |pipeline|
temp_doc.xpath('child::text()').each do |temp_node| temp_doc.xpath('child::text()').each do |temp_node|
html = temp_node.to_html html = temp_node.to_html
temp_node.content.scan(pipeline[:pattern]).each do |matched, math|
html.sub!(matched, math_html(math: math, style: pipeline[:style])) pipeline[:pattern].scan(temp_node.content).each do |match|
math = pipeline[:pattern].extract_named_group(:math, match)
html.sub!(match.first, math_html(math: math, style: pipeline[:style]))
end end
temp_node.replace(html) temp_node.replace(html)

View file

@ -16,31 +16,30 @@ module Banzai
# by converting it into the ```math syntax. In this way, we can ensure # by converting it into the ```math syntax. In this way, we can ensure
# that it's considered a code block and will not have any markdown processed inside it. # that it's considered a code block and will not have any markdown processed inside it.
# Corresponds to the "$$\n...\n$$" syntax
REGEX = %r{
#{::Gitlab::Regex.markdown_code_or_html_blocks}
|
(?=(?<=^\n|\A)\$\$\ *\n.*\n\$\$\ *(?=\n$|\z))(?:
# Display math block: # Display math block:
# $$ # $$
# latex math # latex math
# $$ # $$
REGEX =
(?<=^\n|\A)\$\$\ *\n "#{::Gitlab::Regex.markdown_code_or_html_blocks_or_html_comments_untrusted}" \
(?<display_math> '|' \
(?:.)+? '^\$\$\ *\n' \
) '(?P<display_math>' \
\n\$\$\ *(?=\n$|\z) '(?:\n|.)*?' \
) ')' \
}mx.freeze '\n\$\$\ *$' \
.freeze
def call def call
@text.gsub(REGEX) do regex = Gitlab::UntrustedRegexp.new(REGEX, multiline: true)
if $~[:display_math] return @text unless regex.match?(@text)
regex.replace_gsub(@text) do |match|
# change from $$ to ```math # change from $$ to ```math
"```math\n#{$~[:display_math]}\n```" if match[:display_math]
"```math\n#{match[:display_math]}\n```"
else else
$~[0] match.to_s
end end
end end
end end

View file

@ -6,13 +6,13 @@ module Banzai
def call def call
lang_mapping = Gitlab::FrontMatter::DELIM_LANG lang_mapping = Gitlab::FrontMatter::DELIM_LANG
html.sub(Gitlab::FrontMatter::PATTERN) do |_match| Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.replace_gsub(html) do |match|
lang = $~[:lang].presence || lang_mapping[$~[:delim]] lang = match[:lang].presence || lang_mapping[match[:delim]]
before = $~[:before] before = match[:before]
before = "\n#{before}" if $~[:encoding].presence before = "\n#{before}" if match[:encoding].presence
"#{before}```#{lang}:frontmatter\n#{$~[:front_matter]}```\n" "#{before}```#{lang}:frontmatter\n#{match[:front_matter]}```\n"
end end
end end
end end

View file

@ -6,6 +6,14 @@ module Banzai
class InlineDiffFilter < HTML::Pipeline::Filter class InlineDiffFilter < HTML::Pipeline::Filter
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
INLINE_DIFF_DELETION_UNTRUSTED = '(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})'
INLINE_DIFF_DELETION_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(INLINE_DIFF_DELETION_UNTRUSTED, multiline: false)
INLINE_DIFF_ADDITION_UNTRUSTED = '(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})'
INLINE_DIFF_ADDITION_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(INLINE_DIFF_ADDITION_UNTRUSTED, multiline: false)
def call def call
doc.xpath('descendant-or-self::text()').each do |node| doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
@ -21,8 +29,13 @@ module Banzai
end end
def inline_diff_filter(text) def inline_diff_filter(text)
html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>') html_content = INLINE_DIFF_DELETION_UNTRUSTED_REGEX.replace_gsub(text) do |match|
html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>') %(<span class="idiff left right deletion">#{match[1]}#{match[2]}</span>)
end
INLINE_DIFF_ADDITION_UNTRUSTED_REGEX.replace_gsub(html_content) do |match|
%(<span class="idiff left right addition">#{match[1]}#{match[2]}</span>)
end
end end
end end
end end

View file

@ -157,11 +157,11 @@ module ExtractsRef
end end
def ambiguous_ref?(project, ref) def ambiguous_ref?(project, ref)
return false unless ref
return true if project.repository.ambiguous_ref?(ref) return true if project.repository.ambiguous_ref?(ref)
return false unless ref.starts_with?(%r{(refs|heads|tags)/})
return false unless ref&.starts_with?('refs/') unprefixed_ref = ref.sub(%r{^(refs/)?(heads|tags)/}, '')
unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '')
project.repository.commit(unprefixed_ref).present? project.repository.commit(unprefixed_ref).present?
end end
end end

View file

@ -3,16 +3,16 @@
Dear GitLab user, Dear GitLab user,
%p %p
As part of our commitment to keeping GitLab secure, we have identified and addressed a vulnerability in GitLab that allowed some users to bypass the email verification process in a #{link_to("recent security release", "https://about.gitlab.com/releases/2020/05/27/security-release-13-0-1-released", target: '_blank')}. As part of our commitment to keeping GitLab secure, we have identified and addressed a vulnerability in GitLab that allowed some users to bypass the email verification process in a #{link_to('recent security release', 'https://about.gitlab.com/releases/2020/05/27/security-release-13-0-1-released', target: '_blank')}.
%p %p
As a precautionary measure, you will need to re-verify some of your account's email addresses before continuing to use GitLab. Sorry for the inconvenience! As a precautionary measure, you will need to re-verify some of your account's email addresses before continuing to use GitLab. Sorry for the inconvenience!
%p %p
We have already sent the re-verification email with a subject line of "Confirmation instructions" from #{@verification_from_mail}. Please feel free to contribute any questions or comments to #{link_to("this issue", "https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942", target: '_blank')}. We have already sent the re-verification email with a subject line of 'Confirmation instructions' from #{@verification_from_mail}. Please feel free to contribute any questions or comments to #{link_to('this issue', 'https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942', target: '_blank')}.
%p %p
If you are not "#{@user.username}", please #{link_to 'report this to our administrator', new_abuse_report_url(user_id: @user.id)} If you are not "#{@user.username}", please report abuse from the user's #{link_to('profile page', user_url(@user.id), target: '_blank', rel: 'noopener noreferrer')}. #{link_to('Learn more.', help_page_url('user/report_abuse', anchor: 'report-abuse-from-the-users-profile-page', target: '_blank', rel: 'noopener noreferrer'))}
%p %p
Thank you for being a GitLab user! Thank you for being a GitLab user!

View file

@ -9,6 +9,8 @@ As a precautionary measure, you will need to re-verify some of your account's em
We have already sent the re-verification email with a subject line of "Confirmation instructions" from <%= @verification_from_mail %>. We have already sent the re-verification email with a subject line of "Confirmation instructions" from <%= @verification_from_mail %>.
Please feel free to contribute any questions or comments to this issue: https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942 Please feel free to contribute any questions or comments to this issue: https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942
If you are not "<%= @user.username %>", please report this to our administrator. Report link: <%= new_abuse_report_url(user_id: @user.id) %> If you are not "<%= @user.username %>", please report abuse from the user's profile page: <%= user_url(@user.id) %>.
Learn more: <%= help_page_url('user/report_abuse', anchor: 'report-abuse-from-the-users-profile-page') %>
Thank you for being a GitLab user! Thank you for being a GitLab user!

View file

@ -10,7 +10,8 @@ module Gitlab
'Only a project maintainer or owner can delete a protected tag.', 'Only a project maintainer or owner can delete a protected tag.',
delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.', delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.', create_protected_tag: 'You are not allowed to create this tag as it is protected.',
default_branch_collision: 'You cannot use default branch name to create a tag' default_branch_collision: 'You cannot use default branch name to create a tag',
prohibited_tag_name: 'You cannot create a tag with a prohibited pattern.'
}.freeze }.freeze
LOG_MESSAGES = { LOG_MESSAGES = {
@ -29,11 +30,20 @@ module Gitlab
end end
default_branch_collision_check default_branch_collision_check
prohibited_tag_checks
protected_tag_checks protected_tag_checks
end end
private private
def prohibited_tag_checks
return if deletion?
if tag_name.start_with?("refs/tags/") # rubocop: disable Style/GuardClause
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
end
end
def protected_tag_checks def protected_tag_checks
logger.log_timed(LOG_MESSAGES[__method__]) do logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Artifacts
class DecompressedArtifactSizeValidator
DEFAULT_MAX_BYTES = 4.gigabytes.freeze
FILE_FORMAT_VALIDATORS = {
gzip: ::Gitlab::Ci::DecompressedGzipSizeValidator
}.freeze
FileDecompressionError = Class.new(StandardError)
def initialize(file:, file_format:, max_bytes: DEFAULT_MAX_BYTES)
@file = file
@file_path = file&.path
@file_format = file_format
@max_bytes = max_bytes
end
def validate!
validator_class = FILE_FORMAT_VALIDATORS[file_format.to_sym]
return if file_path.nil?
return if validator_class.nil?
if file.respond_to?(:object_store) && file.object_store == ObjectStorage::Store::REMOTE
return if valid_on_storage?(validator_class)
elsif validator_class.new(archive_path: file_path, max_bytes: max_bytes).valid?
return
end
raise(FileDecompressionError, 'File decompression error')
end
private
attr_reader :file_path, :file, :file_format, :max_bytes
def valid_on_storage?(validator_class)
temp_filename = "#{SecureRandom.uuid}.gz"
is_valid = false
Tempfile.open(temp_filename, '/tmp') do |tempfile|
tempfile.binmode
::Faraday.get(file.url) do |req|
req.options.on_data = proc { |chunk, _| tempfile.write(chunk) }
end
is_valid = validator_class.new(archive_path: tempfile.path, max_bytes: max_bytes).valid?
tempfile.unlink
end
is_valid
end
end
end
end
end

View file

@ -0,0 +1,79 @@
# frozen_string_literal: true
module Gitlab
module Ci
class DecompressedGzipSizeValidator
DEFAULT_MAX_BYTES = 4.gigabytes.freeze
TIMEOUT_LIMIT = 210.seconds
ServiceError = Class.new(StandardError)
def initialize(archive_path:, max_bytes: DEFAULT_MAX_BYTES)
@archive_path = archive_path
@max_bytes = max_bytes
end
def valid?
validate
end
private
def validate
pgrps = nil
valid_archive = true
validate_archive_path
Timeout.timeout(TIMEOUT_LIMIT) do
stderr_r, stderr_w = IO.pipe
stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w)
# When validation is performed on a small archive (e.g. 100 bytes)
# `wait_thr` finishes before we can get process group id. Do not
# raise exception in this scenario.
pgrps = wait_threads.map do |wait_thr|
Process.getpgid(wait_thr[:pid])
rescue Errno::ESRCH
nil
end
pgrps.compact!
status = wait_threads.last.value
if status.success?
result = stdout.readline
valid_archive = false if result.to_i > max_bytes
else
valid_archive = false
end
ensure
stdout.close
stderr_w.close
stderr_r.close
end
valid_archive
rescue StandardError
pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
false
end
def validate_archive_path
Gitlab::Utils.check_path_traversal!(archive_path)
raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink?
raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path)
end
def command
[['gzip', '-dc', archive_path], ['wc', '-c']]
end
attr_reader :archive_path, :max_bytes
end
end
end

View file

@ -8,15 +8,35 @@ module Gitlab
';;;' => 'json' ';;;' => 'json'
}.freeze }.freeze
DELIM = Regexp.union(DELIM_LANG.keys) DELIM_UNTRUSTED = "(?:#{Gitlab::FrontMatter::DELIM_LANG.keys.map { |x| RE2::Regexp.escape(x) }.join('|')})".freeze
PATTERN = %r{ # Original pattern:
\A(?<encoding>[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line # \A(?<encoding>[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line
(?<before>\s*) # (?<before>\s*)
^(?<delim>#{DELIM})[ \t]*(?<lang>\S*)\R # opening front matter marker (optional language specifier) # ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*)\R # opening front matter marker (optional language specifier)
(?<front_matter>.*?) # front matter block content (not greedy) # (?<front_matter>.*?) # front matter block content (not greedy)
^(\k<delim> | \.{3}) # closing front matter marker # ^(\k<delim> | \.{3}) # closing front matter marker
[^\S\r\n]*(\R|\z) # [^\S\r\n]*(\R|\z)
}mx.freeze # rubocop:disable Style/StringConcatenation
# rubocop:disable Style/LineEndConcatenation
PATTERN_UNTRUSTED =
# optional encoding line
"\\A(?P<encoding>[^\\r\\n]*coding:[^\\r\\n]*#{::Gitlab::UntrustedRegexp::BACKSLASH_R})?" +
'(?P<before>\s*)' +
# opening front matter marker (optional language specifier)
"^(?P<delim>#{DELIM_UNTRUSTED})[ \\t]*(?P<lang>\\S*)#{::Gitlab::UntrustedRegexp::BACKSLASH_R}" +
# front matter block content (not greedy)
'(?P<front_matter>(?:\n|.)*?)' +
# closing front matter marker
"^((?P<delim_closing>#{DELIM_UNTRUSTED})|\\.{3})" +
"[^\\S\\r\\n]*(#{::Gitlab::UntrustedRegexp::BACKSLASH_R}|\\z)"
# rubocop:enable Style/LineEndConcatenation
# rubocop:enable Style/StringConcatenation
PATTERN_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(PATTERN_UNTRUSTED, multiline: true)
end end
end end

View file

@ -5,15 +5,14 @@ module Gitlab
class BaseBuilder class BaseBuilder
attr_accessor :object attr_accessor :object
MARKDOWN_SIMPLE_IMAGE = %r{ MARKDOWN_SIMPLE_IMAGE =
#{::Gitlab::Regex.markdown_code_or_html_blocks} "#{::Gitlab::Regex.markdown_code_or_html_blocks_untrusted}" \
| '|' \
(?<image> '(?P<image>' \
! '!' \
\[(?<title>[^\n]*?)\] '\[(?P<title>[^\n]*?)\]' \
\((?<url>(?!(https?://|//))[^\n]+?)\) '\((?P<url>(?P<https>(https?://|//)?)[^\n]+?)\)' \
) ')'.freeze
}mx.freeze
def initialize(object) def initialize(object)
@object = object @object = object
@ -37,15 +36,18 @@ module Gitlab
def absolute_image_urls(markdown_text) def absolute_image_urls(markdown_text)
return markdown_text unless markdown_text.present? return markdown_text unless markdown_text.present?
markdown_text.gsub(MARKDOWN_SIMPLE_IMAGE) do regex = Gitlab::UntrustedRegexp.new(MARKDOWN_SIMPLE_IMAGE, multiline: false)
if $~[:image] return markdown_text unless regex.match?(markdown_text)
url = $~[:url]
regex.replace_gsub(markdown_text) do |match|
if match[:image] && !match[:https]
url = match[:url]
url = "#{uploads_prefix}#{url}" if url.start_with?('/uploads') url = "#{uploads_prefix}#{url}" if url.start_with?('/uploads')
url = "/#{url}" unless url.start_with?('/') url = "/#{url}" unless url.start_with?('/')
"![#{$~[:title]}](#{Gitlab.config.gitlab.url}#{url})" "![#{match[:title]}](#{Gitlab.config.gitlab.url}#{url})"
else else
$~[0] match.to_s
end end
end end
end end

View file

@ -121,29 +121,10 @@ module Gitlab
def authorized_record_json(record, options) def authorized_record_json(record, options)
include_keys = options[:include].flat_map(&:keys) include_keys = options[:include].flat_map(&:keys)
keys_to_authorize = record.try(:restricted_associations, include_keys) keys_to_authorize = record.try(:restricted_associations, include_keys)
return record.to_json(options) if keys_to_authorize.blank? return record.to_json(options) if keys_to_authorize.blank?
record_hash = record.as_json(options).with_indifferent_access record.to_authorized_json(keys_to_authorize, current_user, options)
filtered_record_hash(record, keys_to_authorize, record_hash).to_json(options)
end
def filtered_record_hash(record, keys_to_authorize, record_hash)
keys_to_authorize.each do |key|
next unless record_hash[key].present?
readable = record.try(:readable_records, key, current_user: current_user)
if record.has_many_association?(key)
readable_ids = readable.pluck(:id)
record_hash[key].keep_if do |association_record|
readable_ids.include?(association_record[:id])
end
else
record_hash[key] = nil unless readable.present?
end
end
record_hash
end end
def batch(relation, key) def batch(relation, key)

View file

@ -459,7 +459,7 @@ module Gitlab
# ``` # ```
MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED = MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED =
'(?P<code>' \ '(?P<code>' \
'^```\n' \ '^```.*?\n' \
'(?:\n|.)*?' \ '(?:\n|.)*?' \
'\n```\ *$' \ '\n```\ *$' \
')'.freeze ')'.freeze
@ -477,6 +477,17 @@ module Gitlab
) )
}mx.freeze }mx.freeze
# HTML block:
# <tag>
# Anything, including `>>>` blocks which are ignored by this filter
# </tag>
MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED =
'(?P<html>' \
'^<[^>]+?>\ *\n' \
'(?:\n|.)*?' \
'\n<\/[^>]+?>\ *$' \
')'.freeze
# HTML comment line: # HTML comment line:
# <!-- some commented text --> # <!-- some commented text -->
MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED = MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED =
@ -499,6 +510,13 @@ module Gitlab
}mx.freeze }mx.freeze
end end
def markdown_code_or_html_blocks_untrusted
@markdown_code_or_html_blocks_untrusted ||=
"#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED}"
end
def markdown_code_or_html_comments_untrusted def markdown_code_or_html_comments_untrusted
@markdown_code_or_html_comments_untrusted ||= @markdown_code_or_html_comments_untrusted ||=
"#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \ "#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \
@ -508,6 +526,17 @@ module Gitlab
"#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}" "#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}"
end end
def markdown_code_or_html_blocks_or_html_comments_untrusted
@markdown_code_or_html_comments_untrusted ||=
"#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}"
end
# Based on Jira's project key format # Based on Jira's project key format
# https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html # https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html
# Avoids linking CVE IDs (https://cve.mitre.org/cve/identifiers/syntaxchange.html#new) as Jira issues. # Avoids linking CVE IDs (https://cve.mitre.org/cve/identifiers/syntaxchange.html#new) as Jira issues.

View file

@ -13,6 +13,10 @@ module Gitlab
class UntrustedRegexp class UntrustedRegexp
require_dependency 're2' require_dependency 're2'
# recreate Ruby's \R metacharacter
# https://ruby-doc.org/3.2.2/Regexp.html#class-Regexp-label-Character+Classes
BACKSLASH_R = '(\n|\v|\f|\r|\x{0085}|\x{2028}|\x{2029}|\r\n)'
delegate :===, :source, to: :regexp delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false) def initialize(pattern, multiline: false)
@ -29,6 +33,27 @@ module Gitlab
RE2.GlobalReplace(text, regexp, rewrite) RE2.GlobalReplace(text, regexp, rewrite)
end end
# There is no built-in replace with block support (like `gsub`). We can accomplish
# the same thing by parsing and rebuilding the string with the substitutions.
def replace_gsub(text)
new_text = +''
remainder = text
matched = match(remainder)
until matched.nil? || matched.to_a.compact.empty?
partitioned = remainder.partition(matched.to_s)
new_text << partitioned.first
remainder = partitioned.last
new_text << yield(matched)
matched = match(remainder)
end
new_text << remainder
end
def scan(text) def scan(text)
matches = scan_regexp.scan(text).to_a matches = scan_regexp.scan(text).to_a
matches.map!(&:first) if regexp.number_of_capturing_groups == 0 matches.map!(&:first) if regexp.number_of_capturing_groups == 0

View file

@ -53,7 +53,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(delim = nil, lang = '', text = nil) def initialize(delim = nil, lang = '', text = nil)
@lang = lang.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim] @lang = lang&.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim]
@text = text&.strip! @text = text&.strip!
end end
@ -109,11 +109,17 @@ module Gitlab
end end
def parse_front_matter_block def parse_front_matter_block
wiki_content.match(Gitlab::FrontMatter::PATTERN) { |m| Block.new(m[:delim], m[:lang], m[:front_matter]) } || Block.new if match = Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.match(wiki_content)
Block.new(match[:delim], match[:lang], match[:front_matter])
else
Block.new
end
end end
def strip_front_matter_block def strip_front_matter_block
wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '') Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.replace_gsub(wiki_content) do
''
end
end end
end end
end end

View file

@ -193,39 +193,80 @@ RSpec.describe ProjectsController, feature_category: :projects do
end end
end end
context 'when the default branch name can resolve to another ref' do context 'when the default branch name is ambiguous' do
let!(:project_with_default_branch) do let_it_be(:project_with_default_branch) do
create(:project, :public, :custom_repo, files: ['somefile']).tap do |p| create(:project, :public, :custom_repo, files: ['somefile'])
p.repository.create_branch("refs/heads/refs/heads/#{other_ref}", 'master')
p.change_head("refs/heads/#{other_ref}")
end.reload
end end
let(:other_ref) { 'branch-name' } shared_examples 'ambiguous ref redirects' do
let(:project) { project_with_default_branch }
let(:branch_ref) { "refs/heads/#{ref}" }
let(:repo) { project.repository }
context 'but there is no other ref' do before do
it 'responds with ok' do repo.create_branch(branch_ref, 'master')
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch } repo.change_head(ref)
expect(response).to be_ok
end end
after do
repo.change_head('master')
repo.delete_branch(branch_ref)
end
subject do
get(
:show,
params: {
namespace_id: project.namespace,
id: project
}
)
end
context 'when there is no conflicting ref' do
let(:other_ref) { 'non-existent-ref' }
it { is_expected.to have_gitlab_http_status(:ok) }
end end
context 'and that other ref exists' do context 'and that other ref exists' do
let(:tree_with_default_branch) do let(:other_ref) { 'master' }
branch = project_with_default_branch.repository.find_branch(project_with_default_branch.default_branch)
project_tree_path(project_with_default_branch, branch.target)
end
before do let(:project_default_root_tree_path) do
project_with_default_branch.repository.create_branch(other_ref, 'master') sha = repo.find_branch(project.default_branch).target
project_tree_path(project, sha)
end end
it 'redirects to tree view for the default branch' do it 'redirects to tree view for the default branch' do
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch } is_expected.to redirect_to(project_default_root_tree_path)
expect(response).to redirect_to(tree_with_default_branch)
end end
end end
end end
context 'when ref starts with ref/heads/' do
let(:ref) { "refs/heads/#{other_ref}" }
include_examples 'ambiguous ref redirects'
end
context 'when ref starts with ref/tags/' do
let(:ref) { "refs/tags/#{other_ref}" }
include_examples 'ambiguous ref redirects'
end
context 'when ref starts with heads/' do
let(:ref) { "heads/#{other_ref}" }
include_examples 'ambiguous ref redirects'
end
context 'when ref starts with tags/' do
let(:ref) { "tags/#{other_ref}" }
include_examples 'ambiguous ref redirects'
end
end
end end
describe "when project repository is disabled" do describe "when project repository is disabled" do

View file

@ -20,6 +20,10 @@ FactoryBot.define do
public_email { email } public_email { email }
end end
trait :notification_email do
notification_email { email }
end
trait :private_profile do trait :private_profile do
private_profile { true } private_profile { true }
end end

View file

@ -299,6 +299,32 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
v^2 + w^2 = x^2 v^2 + w^2 = x^2
``` ```
Parsed correctly when between code blocks
```ruby
x = 1
```
$$
a^2+b^2=c^2
$$
```
plaintext
```
Parsed correctly with a mixture of HTML comments and HTML blocks
<!-- sdf -->
$$
a^2+b^2=c^2
$$
<h1>
html
</h1>
### Gollum Tags ### Gollum Tags
- [[linked-resource]] - [[linked-resource]]

View file

@ -67,6 +67,21 @@ describe('SingleFileDiff', () => {
expect(mock.history.get.length).toBe(1); expect(mock.history.get.length).toBe(1);
}); });
it('ignores user-defined diff path attributes', () => {
setHTMLFixture(`
<div class="diff-file">
<div class="diff-content">
<div class="diff-viewer" data-type="simple">
<div class="note-text"><a data-diff-for-path="test/note/path">Test note</a></div>
<div data-diff-for-path="${blobDiffPath}">MOCK CONTENT</div>
</div>
</div>
</div>
`);
const { diffForPath } = new SingleFileDiff(document.querySelector('.diff-file'));
expect(diffForPath).toEqual(blobDiffPath);
});
it('does not load diffs via axios for already expanded diffs', async () => { it('does not load diffs via axios for already expanded diffs', async () => {
setHTMLFixture(` setHTMLFixture(`
<div class="diff-file"> <div class="diff-file">

View file

@ -189,6 +189,7 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
end end
end end
describe 'protects against malicious backtracking' do
it 'fails fast for strings with many spaces' do it 'fails fast for strings with many spaces' do
content = "coding:" + " " * 50_000 + ";" content = "coding:" + " " * 50_000 + ";"
@ -204,4 +205,13 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
Timeout.timeout(3.seconds) { filter(content) } Timeout.timeout(3.seconds) { filter(content) }
end.not_to raise_error end.not_to raise_error
end end
it 'fails fast for strings with many `coding:`' do
content = "coding:" * 120_000 + "\n" * 80_000 + ";"
expect do
Timeout.timeout(3.seconds) { filter(content) }
end.not_to raise_error
end
end
end end

View file

@ -67,4 +67,12 @@ RSpec.describe Banzai::Filter::InlineDiffFilter do
doc = "<tt>START {+something added+} END</tt>" doc = "<tt>START {+something added+} END</tt>"
expect(filter(doc).to_html).to eq(doc) expect(filter(doc).to_html).to eq(doc)
end end
it 'protects against malicious backtracking' do
doc = '[-{-' * 250_000
expect do
Timeout.timeout(3.seconds) { filter(doc) }
end.not_to raise_error
end
end end

View file

@ -100,6 +100,7 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
describe 'block display math using $$\n...\n$$ syntax' do describe 'block display math using $$\n...\n$$ syntax' do
context 'with valid syntax' do context 'with valid syntax' do
where(:text, :result_template) do where(:text, :result_template) do
"$$\n2+2\n$$" | "<math>2+2\n</math>"
"$$ \n2+2\n$$" | "<math>2+2\n</math>" "$$ \n2+2\n$$" | "<math>2+2\n</math>"
"$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4\n</math>" "$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4\n</math>"
end end
@ -214,6 +215,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
expect(doc.search('.js-render-math').count).to eq(2) expect(doc.search('.js-render-math').count).to eq(2)
end end
it 'protects against malicious backtracking' do
doc = pipeline_filter("$$#{' ' * 1_000_000}$")
expect do
Timeout.timeout(3.seconds) { filter(doc) }
end.not_to raise_error
end
def pipeline_filter(text) def pipeline_filter(text)
context = { project: nil, no_sourcepos: true } context = { project: nil, no_sourcepos: true }
doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) doc = Banzai::Pipeline::PreProcessPipeline.call(text, {})

View file

@ -7,6 +7,6 @@ RSpec.describe Gitlab::BackgroundMigration::Mailers::UnconfirmMailer do
let(:subject) { described_class.unconfirm_notification_email(user) } let(:subject) { described_class.unconfirm_notification_email(user) }
it 'contains abuse report url' do it 'contains abuse report url' do
expect(subject.body.encoded).to include(Rails.application.routes.url_helpers.new_abuse_report_url(user_id: user.id)) expect(subject.body.encoded).to include(Gitlab::Routing.url_helpers.user_url(user.id))
end end
end end

View file

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Checks::TagCheck do RSpec.describe Gitlab::Checks::TagCheck, feature_category: :source_code_management do
include_context 'change access checks context' include_context 'change access checks context'
describe '#validate!' do describe '#validate!' do
@ -14,6 +14,29 @@ RSpec.describe Gitlab::Checks::TagCheck do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to change existing tags on this project.') expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to change existing tags on this project.')
end end
context "prohibited tags check" do
it "prohibits tag names that include refs/tags/ at the head" do
allow(subject).to receive(:tag_name).and_return("refs/tags/foo")
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a tag with a prohibited pattern.")
end
it "doesn't prohibit a nested refs/tags/ string in a tag name" do
allow(subject).to receive(:tag_name).and_return("fix-for-refs/tags/foo")
expect { subject.validate! }.not_to raise_error
end
context "deleting a refs/tags headed tag" do
let(:newrev) { "0000000000000000000000000000000000000000" }
let(:ref) { "refs/tags/refs/tags/267208abfe40e546f5e847444276f7d43a39503e" }
it "doesn't prohibit the deletion of a refs/tags/ tag name" do
expect { subject.validate! }.not_to raise_error
end
end
end
context 'with protected tag' do context 'with protected tag' do
let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }

View file

@ -0,0 +1,95 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator, feature_category: :build_artifacts do
include WorkhorseHelpers
let_it_be(:file_path) { File.join(Dir.tmpdir, 'decompressed_archive_size_validator_spec.gz') }
let(:file) { File.open(file_path) }
let(:file_format) { :gzip }
let(:max_bytes) { 20 }
let(:gzip_valid?) { true }
let(:validator) { instance_double(::Gitlab::Ci::DecompressedGzipSizeValidator, valid?: gzip_valid?) }
before(:all) do
Zlib::GzipWriter.open(file_path) do |gz|
gz.write('Hello World!')
end
end
after(:all) do
FileUtils.rm(file_path)
end
before do
allow(::Gitlab::Ci::DecompressedGzipSizeValidator)
.to receive(:new)
.and_return(validator)
end
subject { described_class.new(file: file, file_format: file_format, max_bytes: max_bytes) }
shared_examples 'when file does not exceed allowed compressed size' do
let(:gzip_valid?) { true }
it 'passes validation' do
expect { subject.validate! }.not_to raise_error
end
end
shared_examples 'when file exceeds allowed decompressed size' do
let(:gzip_valid?) { false }
it 'raises an exception' do
expect { subject.validate! }
.to raise_error(Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator::FileDecompressionError)
end
end
describe '#validate!' do
it_behaves_like 'when file does not exceed allowed compressed size'
it_behaves_like 'when file exceeds allowed decompressed size'
end
context 'when file is not provided' do
let(:file) { nil }
it 'passes validation' do
expect { subject.validate! }.not_to raise_error
end
end
context 'when the file is located in the cloud' do
let(:remote_path) { File.join(remote_store_path, remote_id) }
let(:file_url) { "http://s3.amazonaws.com/#{remote_path}" }
let(:file) do
instance_double(JobArtifactUploader,
path: file_path,
url: file_url,
object_store: ObjectStorage::Store::REMOTE)
end
let(:remote_id) { 'generated-remote-id-12345' }
let(:remote_store_path) { ObjectStorage::TMP_UPLOAD_PATH }
before do
stub_request(:get, %r{s3.amazonaws.com/#{remote_path}})
.to_return(status: 200, body: File.read('spec/fixtures/build.env.gz'))
end
it_behaves_like 'when file does not exceed allowed compressed size'
it_behaves_like 'when file exceeds allowed decompressed size'
end
context 'when file_format is not on the list' do
let_it_be(:file_format) { 'rar' }
it 'passes validation' do
expect { subject.validate! }.not_to raise_error
end
end
end

View file

@ -0,0 +1,127 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::DecompressedGzipSizeValidator, feature_category: :importers do
let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_gzip_size_validator_spec.gz') }
before(:all) do
create_compressed_file
end
after(:all) do
FileUtils.rm(filepath)
end
subject { described_class.new(archive_path: filepath, max_bytes: max_bytes) }
describe '#valid?' do
let(:max_bytes) { 20 }
context 'when file does not exceed allowed decompressed size' do
it 'returns true' do
expect(subject.valid?).to eq(true)
end
context 'when the waiter thread no longer exists due to being terminated or crashing' do
it 'gracefully handles the absence of the waiter without raising exception' do
allow(Process).to receive(:getpgid).and_raise(Errno::ESRCH)
expect(subject.valid?).to eq(true)
end
end
end
context 'when file exceeds allowed decompressed size' do
let(:max_bytes) { 1 }
it 'returns false' do
expect(subject.valid?).to eq(false)
end
end
context 'when exception occurs during header readings' do
shared_examples 'raises exception and terminates validator process group' do
let(:std) { instance_double(IO, close: nil) }
let(:wait_thr) { double }
let(:wait_threads) { [wait_thr, wait_thr] }
before do
allow(Process).to receive(:getpgid).and_return(2)
allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads])
allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
allow(wait_thr).to receive(:value).and_raise(exception)
end
it 'terminates validator process group' do
expect(Process).to receive(:kill).with(-1, 2).twice
expect(subject.valid?).to eq(false)
end
end
context 'when timeout occurs' do
let(:exception) { Timeout::Error }
include_examples 'raises exception and terminates validator process group'
end
context 'when exception occurs' do
let(:error_message) { 'Error!' }
let(:exception) { StandardError.new(error_message) }
include_examples 'raises exception and terminates validator process group'
end
end
describe 'archive path validation' do
let(:filesize) { nil }
context 'when archive path is traversed' do
let(:filepath) { '/foo/../bar' }
it 'does not pass validation' do
expect(subject.valid?).to eq(false)
end
end
end
context 'when archive path is not a string' do
let(:filepath) { 123 }
it 'returns false' do
expect(subject.valid?).to eq(false)
end
end
context 'when archive path is a symlink' do
let(:filepath) { File.join(Dir.tmpdir, 'symlink') }
before do
FileUtils.ln_s(filepath, filepath, force: true)
end
it 'returns false' do
expect(subject.valid?).to eq(false)
end
end
context 'when archive path is not a file' do
let(:filepath) { Dir.mktmpdir }
let(:filesize) { File.size(filepath) }
after do
FileUtils.rm_rf(filepath)
end
it 'returns false' do
expect(subject.valid?).to eq(false)
end
end
end
def create_compressed_file
Zlib::GzipWriter.open(filepath) do |gz|
gz.write('Hello World!')
end
end
end

View file

@ -7,6 +7,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let_it_be(:exportable_path) { 'project' } let_it_be(:exportable_path) { 'project' }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:private_project) { create(:project, :private, group: group) }
let_it_be(:private_mr) { create(:merge_request, source_project: private_project, project: private_project) }
let_it_be(:project) { setup_project } let_it_be(:project) { setup_project }
shared_examples 'saves project tree successfully' do |ndjson_enabled| shared_examples 'saves project tree successfully' do |ndjson_enabled|
@ -125,6 +127,13 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
expect(reviewer).not_to be_nil expect(reviewer).not_to be_nil
expect(reviewer['user_id']).to eq(user.id) expect(reviewer['user_id']).to eq(user.id)
end end
it 'has merge requests system notes' do
system_notes = subject.first['notes'].select { |note| note['system'] }
expect(system_notes.size).to eq(1)
expect(system_notes.first['note']).to eq('merged')
end
end end
context 'with snippets' do context 'with snippets' do
@ -512,6 +521,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
create(:milestone, project: project) create(:milestone, project: project)
discussion_note = create(:discussion_note, noteable: issue, project: project) discussion_note = create(:discussion_note, noteable: issue, project: project)
mr_note = create(:note, noteable: merge_request, project: project) mr_note = create(:note, noteable: merge_request, project: project)
create(:system_note, noteable: merge_request, project: project, author: user, note: 'merged')
private_system_note = "mentioned in merge request #{private_mr.to_reference(project)}"
create(:system_note, noteable: merge_request, project: project, author: user, note: private_system_note)
create(:note, noteable: snippet, project: project) create(:note, noteable: snippet, project: project)
create(:note_on_commit, create(:note_on_commit,
author: user, author: user,

View file

@ -1164,12 +1164,23 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
MARKDOWN MARKDOWN
end end
describe 'normal regular expression' do
it { is_expected.to match(%(<section>\nsomething\n</section>)) } it { is_expected.to match(%(<section>\nsomething\n</section>)) }
it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) }
it { is_expected.not_to match(%(<section>must be multi-line</section>)) } it { is_expected.not_to match(%(<section>must be multi-line</section>)) }
it { expect(subject.match(markdown)[:html]).to eq expected } it { expect(subject.match(markdown)[:html]).to eq expected }
end end
describe 'untrusted regular expression' do
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED, multiline: true) }
it { is_expected.to match(%(<section>\nsomething\n</section>)) }
it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) }
it { is_expected.not_to match(%(<section>must be multi-line</section>)) }
it { expect(subject.match(markdown)[:html]).to eq expected }
end
end
context 'HTML comment lines' do context 'HTML comment lines' do
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED, multiline: true) } subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED, multiline: true) }

View file

@ -3,7 +3,7 @@
require 'fast_spec_helper' require 'fast_spec_helper'
require 'support/shared_examples/lib/gitlab/malicious_regexp_shared_examples' require 'support/shared_examples/lib/gitlab/malicious_regexp_shared_examples'
RSpec.describe Gitlab::UntrustedRegexp do RSpec.describe Gitlab::UntrustedRegexp, feature_category: :shared do
describe '#initialize' do describe '#initialize' do
subject { described_class.new(pattern) } subject { described_class.new(pattern) }
@ -22,6 +22,39 @@ RSpec.describe Gitlab::UntrustedRegexp do
end end
end end
describe '#replace_gsub' do
let(:regex_str) { '(?P<scheme>(ftp))' }
let(:regex) { described_class.new(regex_str, multiline: true) }
def result(regex, text)
regex.replace_gsub(text) do |match|
if match[:scheme]
"http|#{match[:scheme]}|rss"
else
match.to_s
end
end
end
it 'replaces all instances of the match in a string' do
text = 'Use only https instead of ftp'
expect(result(regex, text)).to eq('Use only https instead of http|ftp|rss')
end
it 'replaces nothing when no match' do
text = 'Use only https instead of gopher'
expect(result(regex, text)).to eq(text)
end
it 'handles empty text' do
text = ''
expect(result(regex, text)).to eq('')
end
end
describe '#replace' do describe '#replace' do
it 'replaces the first instance of the match in a string' do it 'replaces the first instance of the match in a string' do
result = described_class.new('foo').replace('foo bar foo', 'oof') result = described_class.new('foo').replace('foo bar foo', 'oof')

View file

@ -36,6 +36,21 @@ RSpec.describe Ci::Artifactable do
expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times
end end
end end
context 'when decompressed artifact size validator fails' do
let(:artifact) { build(:ci_job_artifact, :junit) }
before do
allow_next_instance_of(Gitlab::Ci::DecompressedGzipSizeValidator) do |instance|
allow(instance).to receive(:valid?).and_return(false)
end
end
it 'fails on blob' do
expect { |b| artifact.each_blob(&b) }
.to raise_error(::Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator::FileDecompressionError)
end
end
end end
context 'when file format is raw' do context 'when file format is raw' do

View file

@ -9,6 +9,7 @@ RSpec.describe Exportable, feature_category: :importers do
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) } let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
let_it_be(:note1) { create(:system_note, project: project, noteable: issue) } let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
let_it_be(:note2) { create(:system_note, project: project, noteable: issue) } let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
let_it_be(:options) { { include: [{ notes: { only: [:note] }, milestone: { only: :title } }] } }
let_it_be(:model_klass) do let_it_be(:model_klass) do
Class.new(ApplicationRecord) do Class.new(ApplicationRecord) do
@ -28,19 +29,27 @@ RSpec.describe Exportable, feature_category: :importers do
subject { model_klass.new } subject { model_klass.new }
describe '.readable_records' do describe '.to_authorized_json' do
let_it_be(:model_record) { model_klass.new } let_it_be(:model_record) { model_klass.new }
context 'when model does not respond to association name' do context 'when key to authorize is not an association name' do
it 'returns nil' do it 'returns string without given key' do
expect(subject.readable_records(:foo, current_user: user)).to be_nil expect(subject.to_authorized_json([:foo], user, options)).not_to include('foo')
end end
end end
context 'when model does respond to association name' do context 'when key to authorize is an association name' do
let(:key_to_authorize) { :notes }
subject(:record_json) { model_record.to_authorized_json([key_to_authorize], user, options) }
context 'when there are no records' do context 'when there are no records' do
it 'returns nil' do before do
expect(model_record.readable_records(:notes, current_user: user)).to be_nil allow(model_record).to receive(:notes).and_return(Note.none)
end
it 'returns string including the empty association' do
expect(record_json).to include("\"notes\":[]")
end end
end end
@ -57,8 +66,9 @@ RSpec.describe Exportable, feature_category: :importers do
end end
end end
it 'returns collection of readable records' do it 'returns string containing all records' do
expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2) expect(record_json)
.to include("\"notes\":[{\"note\":\"#{note1.note}\"},{\"note\":\"#{note2.note}\"}]")
end end
end end
@ -70,8 +80,19 @@ RSpec.describe Exportable, feature_category: :importers do
end end
end end
it 'returns collection of readable records' do it 'returns string including the empty association' do
expect(model_record.readable_records(:notes, current_user: user)).to eq([]) expect(record_json).to include("\"notes\":[]")
end
end
context 'when user can read some records' do
before do
allow(model_record).to receive(:readable_records).with(:notes, current_user: user)
.and_return([note1])
end
it 'returns string containing readable records only' do
expect(record_json).to include("\"notes\":[{\"note\":\"#{note1.note}\"}]")
end end
end end
end end
@ -87,13 +108,15 @@ RSpec.describe Exportable, feature_category: :importers do
it 'calls #readable_by?' do it 'calls #readable_by?' do
expect(note1).to receive(:readable_by?).with(user) expect(note1).to receive(:readable_by?).with(user)
model_record.readable_records(:notes, current_user: user) record_json
end end
end end
context 'with single relation' do context 'with single relation' do
let(:key_to_authorize) { :milestone }
before do before do
allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone) allow(model_record).to receive(:milestone).and_return(issue.milestone)
end end
context 'when user can read the record' do context 'when user can read the record' do
@ -101,8 +124,8 @@ RSpec.describe Exportable, feature_category: :importers do
allow(milestone).to receive(:readable_by?).with(user).and_return(true) allow(milestone).to receive(:readable_by?).with(user).and_return(true)
end end
it 'returns collection of readable records' do it 'returns string including association' do
expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone) expect(record_json).to include("\"milestone\":{\"title\":\"#{milestone.title}\"}")
end end
end end
@ -111,8 +134,8 @@ RSpec.describe Exportable, feature_category: :importers do
allow(milestone).to receive(:readable_by?).with(user).and_return(false) allow(milestone).to receive(:readable_by?).with(user).and_return(false)
end end
it 'returns collection of readable records' do it 'returns string with null association' do
expect(model_record.readable_records(:milestone, current_user: user)).to be_nil expect(record_json).to include("\"milestone\":null")
end end
end end
end end
@ -211,26 +234,4 @@ RSpec.describe Exportable, feature_category: :importers do
end end
end end
end end
describe '.has_many_association?' do
let(:model_associations) { [:notes, :labels] }
context 'when association type is `has_many`' do
it 'returns true' do
expect(subject.has_many_association?(:notes)).to eq(true)
end
end
context 'when association type is `has_one`' do
it 'returns true' do
expect(subject.has_many_association?(:milestone)).to eq(false)
end
end
context 'when association type is `belongs_to`' do
it 'returns true' do
expect(subject.has_many_association?(:project)).to eq(false)
end
end
end
end end

View file

@ -1079,4 +1079,22 @@ RSpec.describe Issuable do
end end
end end
end end
context 'with exportable associations' do
let_it_be(:project) { create(:project, group: create(:group, :private)) }
context 'for issues' do
let_it_be_with_reload(:resource) { create(:issue, project: project) }
it_behaves_like 'an exportable'
end
context 'for merge requests' do
let_it_be_with_reload(:resource) do
create(:merge_request, source_project: project, project: project)
end
it_behaves_like 'an exportable'
end
end
end end

View file

@ -44,6 +44,122 @@ RSpec.describe Label do
is_expected.to allow_value("customer's request").for(:title) is_expected.to allow_value("customer's request").for(:title)
is_expected.to allow_value('s' * 255).for(:title) is_expected.to allow_value('s' * 255).for(:title)
end end
describe 'description length' do
let(:invalid_description) { 'x' * (::Label::DESCRIPTION_LENGTH_MAX + 1) }
let(:valid_description) { 'short description' }
let(:label) { build(:label, project: project, description: description) }
let(:error_message) do
format(
_('is too long (%{size}). The maximum size is %{max_size}.'),
size: ActiveSupport::NumberHelper.number_to_human_size(invalid_description.bytesize),
max_size: ActiveSupport::NumberHelper.number_to_human_size(::Label::DESCRIPTION_LENGTH_MAX)
)
end
subject(:validate) { label.validate }
context 'when label is a new record' do
context 'when description exceeds the maximum size' do
let(:description) { invalid_description }
it 'adds a description too long error' do
validate
expect(label.errors[:description]).to contain_exactly(error_message)
end
end
context 'when description is within the allowed limits' do
let(:description) { valid_description }
it 'does not add a validation error' do
validate
expect(label.errors).not_to have_key(:description)
end
end
end
context 'when label is an existing record' do
before do
label.description = existing_description
label.save!(validate: false)
label.description = description
end
context 'when record already had a valid description' do
let(:existing_description) { 'small difference so it triggers description_changed?' }
context 'when new description exceeds the maximum size' do
let(:description) { invalid_description }
it 'adds a description too long error' do
validate
expect(label.errors[:description]).to contain_exactly(error_message)
end
end
context 'when new description is within the allowed limits' do
let(:description) { valid_description }
it 'does not add a validation error' do
validate
expect(label.errors).not_to have_key(:description)
end
end
end
context 'when record existed with an invalid description' do
let(:existing_description) { "#{invalid_description} small difference so it triggers description_changed?" }
context 'when description is not changed' do
let(:description) { existing_description }
it 'does not add a validation error' do
validate
expect(label.errors).not_to have_key(:description)
end
end
context 'when new description exceeds the maximum size' do
context 'when new description is shorter than existing description' do
let(:description) { invalid_description }
it 'allows updating descriptions that already existed above the limit' do
validate
expect(label.errors).not_to have_key(:description)
end
end
context 'when new description is longer than existing description' do
let(:description) { "#{existing_description}1" }
it 'adds a description too long error' do
validate
expect(label.errors[:description]).to contain_exactly(error_message)
end
end
end
context 'when new description is within the allowed limits' do
let(:description) { valid_description }
it 'does not add a validation error' do
validate
expect(label.errors).not_to have_key(:description)
end
end
end
end
end
end end
describe 'scopes' do describe 'scopes' do

View file

@ -106,36 +106,6 @@ RSpec.describe ProjectMember do
end end
end end
describe '.import_team' do
before do
@project_1 = create(:project)
@project_2 = create(:project)
@user_1 = create :user
@user_2 = create :user
@project_1.add_developer(@user_1)
@project_2.add_reporter(@user_2)
@status = @project_2.team.import(@project_1)
end
it { expect(@status).to be_truthy }
describe 'project 2 should get user 1 as developer. user_2 should not be changed' do
it { expect(@project_2.users).to include(@user_1) }
it { expect(@project_2.users).to include(@user_2) }
it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
end
describe 'project 1 should not be changed' do
it { expect(@project_1.users).to include(@user_1) }
it { expect(@project_1.users).not_to include(@user_2) }
end
end
describe '.add_members_to_projects' do describe '.add_members_to_projects' do
it 'adds the given users to the given projects' do it 'adds the given users to the given projects' do
projects = create_list(:project, 2) projects = create_list(:project, 2)

View file

@ -164,6 +164,57 @@ RSpec.describe ProjectTeam, feature_category: :subgroups do
end end
end end
describe '#import_team' do
let_it_be(:source_project) { create(:project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:source_project_owner) { source_project.first_owner }
let_it_be(:source_project_developer) { create(:user) { |user| source_project.add_developer(user) } }
let_it_be(:current_user) { create(:user) { |user| target_project.add_maintainer(user) } }
subject(:import) { target_project.team.import(source_project, current_user) }
it { is_expected.to be_truthy }
it 'target project includes source member with the same access' do
import
imported_member_access = target_project.members.find_by!(user: source_project_developer).access_level
expect(imported_member_access).to eq(Gitlab::Access::DEVELOPER)
end
it 'does not change the source project members' do
import
expect(source_project.users).to include(source_project_developer)
expect(source_project.users).not_to include(current_user)
end
shared_examples 'imports source owners with correct access' do
specify do
import
source_owner_access_in_target = target_project.members.find_by!(user: source_project_owner).access_level
expect(source_owner_access_in_target).to eq(target_access_level)
end
end
context 'when importer is a maintainer in target project' do
it_behaves_like 'imports source owners with correct access' do
let(:target_access_level) { Gitlab::Access::MAINTAINER }
end
end
context 'when importer is an owner in target project' do
before do
target_project.add_owner(current_user)
end
it_behaves_like 'imports source owners with correct access' do
let(:target_access_level) { Gitlab::Access::OWNER }
end
end
end
describe '#find_member' do describe '#find_member' do
context 'personal project' do context 'personal project' do
let(:project) do let(:project) do

View file

@ -659,34 +659,116 @@ RSpec.describe User, feature_category: :user_profile do
end end
end end
describe '#commit_email=' do shared_examples 'for user notification, public, and commit emails' do
subject(:user) { create(:user) } context 'when confirmed primary email' do
let(:user) { create(:user) }
let(:email) { user.email }
it 'can be set to a confirmed email' do it 'can be set' do
confirmed = create(:email, :confirmed, user: user) set_email
user.commit_email = confirmed.email
expect(user).to be_valid expect(user).to be_valid
end end
it 'can not be set to an unconfirmed email' do context 'when primary email is changed' do
unconfirmed = create(:email, user: user) before do
user.commit_email = unconfirmed.email user.email = generate(:email)
end
it 'can not be set' do
set_email
expect(user).not_to be_valid
end
end
context 'when confirmed secondary email' do
let(:email) { create(:email, :confirmed, user: user).email }
it 'can be set' do
set_email
expect(user).to be_valid
end
end
context 'when unconfirmed secondary email' do
let(:email) { create(:email, user: user).email }
it 'can not be set' do
set_email
expect(user).not_to be_valid
end
end
context 'when invalid confirmed secondary email' do
let(:email) { create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') }
it 'can not be set' do
set_email
expect(user).not_to be_valid
end
end
end
context 'when unconfirmed primary email ' do
let(:user) { create(:user, :unconfirmed) }
let(:email) { user.email }
it 'can not be set' do
set_email
expect(user).not_to be_valid
end
end
context 'when new record' do
let(:user) { build(:user, :unconfirmed) }
let(:email) { user.email }
it 'can not be set' do
set_email
expect(user).not_to be_valid expect(user).not_to be_valid
end end
it 'can not be set to a non-existent email' do context 'when skipping confirmation' do
user.commit_email = 'non-existent-email@nonexistent.nonexistent' before do
user.skip_confirmation = true
expect(user).not_to be_valid
end end
it 'can not be set to an invalid email, even if confirmed' do it 'can be set' do
confirmed = create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') set_email
user.commit_email = confirmed.email
expect(user).not_to be_valid expect(user).to be_valid
end
end
end
end
describe 'notification_email' do
include_examples 'for user notification, public, and commit emails' do
subject(:set_email) do
user.notification_email = email
end
end
end
describe 'public_email' do
include_examples 'for user notification, public, and commit emails' do
subject(:set_email) do
user.public_email = email
end
end
end
describe 'commit_email' do
include_examples 'for user notification, public, and commit emails' do
subject(:set_email) do
user.commit_email = email
end
end end
end end
@ -3440,15 +3522,40 @@ RSpec.describe User, feature_category: :user_profile do
describe '#verified_emails' do describe '#verified_emails' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:confirmed_email) { create(:email, :confirmed, user: user) }
before do
create(:email, user: user)
end
it 'returns only confirmed emails' do it 'returns only confirmed emails' do
email_confirmed = create :email, user: user, confirmed_at: Time.current
create :email, user: user
expect(user.verified_emails).to contain_exactly( expect(user.verified_emails).to contain_exactly(
user.email, user.email,
user.private_commit_email, user.private_commit_email,
email_confirmed.email confirmed_email.email
)
end
it 'does not return primary email when primary email is changed' do
original_email = user.email
user.email = generate(:email)
expect(user.verified_emails).to contain_exactly(
user.private_commit_email,
confirmed_email.email,
original_email
)
end
it 'does not return unsaved primary email even if skip_confirmation is enabled' do
original_email = user.email
user.skip_confirmation = true
user.email = generate(:email)
expect(user.verified_emails).to contain_exactly(
user.private_commit_email,
confirmed_email.email,
original_email
) )
end end
end end

View file

@ -18,43 +18,6 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
sign_in(reporter) sign_in(reporter)
end end
describe 'GET new' do
let(:ref_url) { 'http://example.com' }
it 'sets the instance variables' do
get new_abuse_report_path(user_id: user.id, ref_url: ref_url)
expect(assigns(:abuse_report)).to be_kind_of(AbuseReport)
expect(assigns(:abuse_report)).to have_attributes(
user_id: user.id,
reported_from_url: ref_url
)
end
context 'when the user has already been deleted' do
it 'redirects the reporter to root_path' do
user_id = user.id
user.destroy!
get new_abuse_report_path(user_id: user_id)
expect(response).to redirect_to root_path
expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.'))
end
end
context 'when the user has already been blocked' do
it 'redirects the reporter to the user\'s profile' do
user.block
get new_abuse_report_path(user_id: user.id)
expect(response).to redirect_to user
expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.'))
end
end
end
describe 'POST add_category', :aggregate_failures do describe 'POST add_category', :aggregate_failures do
subject(:request) { post add_category_abuse_reports_path, params: request_params } subject(:request) { post add_category_abuse_reports_path, params: request_params }

View file

@ -13,11 +13,12 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
describe 'GET /api/v4/packages/npm/*package_name' do describe 'GET /api/v4/packages/npm/*package_name' do
let(:url) { api("/packages/npm/#{package_name}") } let(:url) { api("/packages/npm/#{package_name}") }
it_behaves_like 'handling get metadata requests', scope: :instance
context 'with a duplicate package name in another project' do
subject { get(url) } subject { get(url) }
it_behaves_like 'handling get metadata requests', scope: :instance
it_behaves_like 'rejects invalid package names'
context 'with a duplicate package name in another project' do
let_it_be(:project2) { create(:project, :public, namespace: namespace) } let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:package2) do let_it_be(:package2) do
create(:npm_package, create(:npm_package,

View file

@ -21,6 +21,9 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'handling get metadata requests', scope: :project it_behaves_like 'handling get metadata requests', scope: :project
it_behaves_like 'accept get request on private project with access to package registry for everyone' it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'rejects invalid package names' do
subject { get(url) }
end
end end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do

View file

@ -351,14 +351,6 @@ RSpec.describe InvitesController, 'routing' do
end end
end end
RSpec.describe AbuseReportsController, 'routing' do
let_it_be(:user) { create(:user) }
it 'to #new' do
expect(get("/-/abuse_reports/new?user_id=#{user.id}")).to route_to('abuse_reports#new', user_id: user.id.to_s)
end
end
RSpec.describe SentNotificationsController, 'routing' do RSpec.describe SentNotificationsController, 'routing' do
it 'to #unsubscribe' do it 'to #unsubscribe' do
expect(get("/-/sent_notifications/4bee17d4a63ed60cf5db53417e9aeb4c/unsubscribe")) expect(get("/-/sent_notifications/4bee17d4a63ed60cf5db53417e9aeb4c/unsubscribe"))

View file

@ -202,7 +202,7 @@ module MarkdownMatchers
match do |actual| match do |actual|
expect(actual).to have_selector('[data-math-style="inline"]', count: 4) expect(actual).to have_selector('[data-math-style="inline"]', count: 4)
expect(actual).to have_selector('[data-math-style="display"]', count: 4) expect(actual).to have_selector('[data-math-style="display"]', count: 6)
end end
end end

View file

@ -6,7 +6,6 @@ RSpec.shared_examples 'reportable note' do |type|
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
let(:more_actions_selector) { '.more-actions.dropdown' } let(:more_actions_selector) { '.more-actions.dropdown' }
let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) }
it 'has an edit button' do it 'has an edit button' do
expect(comment).to have_selector('.js-note-edit') expect(comment).to have_selector('.js-note-edit')

View file

@ -1,27 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'resource with exportable associations' do RSpec.shared_examples 'an exportable' do |restricted_association: :project|
before do let_it_be(:user) { create(:user) }
stub_licensed_features(stubbed_features) if stubbed_features.any?
end
describe '#exportable_association?' do describe '#exportable_association?' do
let(:association) { single_association } let(:association) { restricted_association }
subject { resource.exportable_association?(association, current_user: user) } subject { resource.exportable_association?(association, current_user: user) }
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
context 'when user can read resource' do context 'when user can only read resource' do
before do before do
group.add_developer(user) allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :"read_#{resource.to_ability_name}", resource)
.and_return(true)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
context "when user can read resource's association" do context "when user can read resource's association" do
before do before do
other_group.add_developer(user) allow(resource).to receive(:readable_record?).with(anything, user).and_return(true)
end end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
@ -31,41 +32,48 @@ RSpec.shared_examples 'resource with exportable associations' do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'for an unauthenticated user' do
let(:user) { nil }
it { is_expected.to be_falsey }
end
end end
end end
end end
describe '#readable_records' do describe '#to_authorized_json' do
subject { resource.readable_records(association, current_user: user) } let(:options) { { include: [{ notes: { only: [:id] } }] } }
subject { resource.to_authorized_json(keys, user, options) }
before do before do
group.add_developer(user) allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :"read_#{resource.to_ability_name}", resource)
.and_return(true)
end end
context 'when association not supported' do context 'when association not supported' do
let(:association) { :foo } let(:keys) { [:foo] }
it { is_expected.to be_nil } it { is_expected.not_to include('foo') }
end end
context 'when association is `:notes`' do context 'when association is `:notes`' do
let(:association) { :notes } let_it_be(:readable_note) { create(:system_note, noteable: resource, project: project, note: 'readable') }
let_it_be(:restricted_note) { create(:system_note, noteable: resource, project: project, note: 'restricted') }
it { is_expected.to match_array([readable_note]) } let(:restricted_note_access) { false }
let(:keys) { [:notes] }
context 'when user have access' do
before do before do
other_group.add_developer(user) allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :read_note, readable_note).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_note, restricted_note).and_return(restricted_note_access)
end end
it 'returns all records' do it { is_expected.to include("\"notes\":[{\"id\":#{readable_note.id}}]") }
is_expected.to match_array([readable_note, restricted_note])
context 'when user have access to all notes' do
let(:restricted_note_access) { true }
it 'string includes all notes' do
is_expected.to include("\"notes\":[{\"id\":#{readable_note.id}},{\"id\":#{restricted_note.id}}]")
end end
end end
end end

View file

@ -856,3 +856,14 @@ RSpec.shared_examples 'handling different package names, visibilities and user r
it_behaves_like example_name, status: status it_behaves_like example_name, status: status
end end
end end
RSpec.shared_examples 'rejects invalid package names' do
let(:package_name) { "%0d%0ahttp:/%2fexample.com" }
it do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(Gitlab::Json.parse(response.body)).to eq({ 'error' => 'package_name should be a valid file path' })
end
end