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 # Display math block:
REGEX = %r{ # $$
#{::Gitlab::Regex.markdown_code_or_html_blocks} # latex math
| # $$
(?=(?<=^\n|\A)\$\$\ *\n.*\n\$\$\ *(?=\n$|\z))(?: REGEX =
# Display math block: "#{::Gitlab::Regex.markdown_code_or_html_blocks_or_html_comments_untrusted}" \
# $$ '|' \
# latex math '^\$\$\ *\n' \
# $$ '(?P<display_math>' \
'(?:\n|.)*?' \
(?<=^\n|\A)\$\$\ *\n ')' \
(?<display_math> '\n\$\$\ *$' \
(?:.)+? .freeze
)
\n\$\$\ *(?=\n$|\z)
)
}mx.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)
# change from $$ to ```math
"```math\n#{$~[:display_math]}\n```" regex.replace_gsub(@text) do |match|
# change from $$ to ```math
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,37 +193,78 @@ 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 }
context 'but there is no other ref' do let(:branch_ref) { "refs/heads/#{ref}" }
it 'responds with ok' do let(:repo) { project.repository }
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
expect(response).to be_ok
end
end
context 'and that other ref exists' do
let(:tree_with_default_branch) do
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 before do
project_with_default_branch.repository.create_branch(other_ref, 'master') repo.create_branch(branch_ref, 'master')
repo.change_head(ref)
end end
it 'redirects to tree view for the default branch' do after do
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch } repo.change_head('master')
expect(response).to redirect_to(tree_with_default_branch) repo.delete_branch(branch_ref)
end 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
context 'and that other ref exists' do
let(:other_ref) { 'master' }
let(:project_default_root_tree_path) do
sha = repo.find_branch(project.default_branch).target
project_tree_path(project, sha)
end
it 'redirects to tree view for the default branch' do
is_expected.to redirect_to(project_default_root_tree_path)
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
end end

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

@ -1,294 +1,294 @@
User-Agent: Microsoft-MacOutlook/10.22.0.200209 User-Agent: Microsoft-MacOutlook/10.22.0.200209
Date: Mon, 17 Feb 2020 22:56:47 +0100 Date: Mon, 17 Feb 2020 22:56:47 +0100
Subject: Re: htmltest | test issue (#1) Subject: Re: htmltest | test issue (#1)
From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)" From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)"
<diego.louzan.ext@siemens.com> <diego.louzan.ext@siemens.com>
To: Administrator / htmltest To: Administrator / htmltest
<dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com> <dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com>
Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com> Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com>
Thread-Topic: htmltest | test issue (#1) Thread-Topic: htmltest | test issue (#1)
References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254> References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254>
<issue_451@169.254.169.254> <issue_451@169.254.169.254>
<note_1797@169.254.169.254> <note_1797@169.254.169.254>
In-Reply-To: <note_1797@169.254.169.254> In-Reply-To: <note_1797@169.254.169.254>
Content-type: multipart/signed; Content-type: multipart/signed;
protocol="application/pkcs7-signature"; protocol="application/pkcs7-signature";
micalg=sha256; micalg=sha256;
boundary="B_3664825007_1904734766" boundary="B_3664825007_1904734766"
MIME-Version: 1.0 MIME-Version: 1.0
--B_3664825007_1904734766 --B_3664825007_1904734766
Content-type: multipart/mixed; Content-type: multipart/mixed;
boundary="B_3664825007_384940722" boundary="B_3664825007_384940722"
--B_3664825007_384940722 --B_3664825007_384940722
Content-type: multipart/alternative; Content-type: multipart/alternative;
boundary="B_3664825007_1519466360" boundary="B_3664825007_1519466360"
--B_3664825007_1519466360 --B_3664825007_1519466360
Content-type: text/plain; Content-type: text/plain;
charset="UTF-8" charset="UTF-8"
Content-transfer-encoding: quoted-printable Content-transfer-encoding: quoted-printable
Me too, with an attachment Me too, with an attachment
=20 =20
From: Administrator <dlouzan.dummy@gmail.com> From: Administrator <dlouzan.dummy@gmail.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64= Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64=
223d363@gmail.com> 223d363@gmail.com>
Date: Monday, 17 February 2020 at 22:55 Date: Monday, 17 February 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen= To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen=
s.com> s.com>
Subject: Re: htmltest | test issue (#1) Subject: Re: htmltest | test issue (#1)
=20 =20
Administrator commented:=20 Administrator commented:=20
I pity the foo !!! I pity the foo !!!
=E2=80=94=20 =E2=80=94=20
Reply to this email directly or view it on GitLab.=20 Reply to this email directly or view it on GitLab.=20
You're receiving this email because of your account on 169.254.169.254. If = You're receiving this email because of your account on 169.254.169.254. If =
you'd like to receive fewer emails, you can unsubscribe from this thread or = you'd like to receive fewer emails, you can unsubscribe from this thread or =
adjust your notification settings.=20 adjust your notification settings.=20
--B_3664825007_1519466360 --B_3664825007_1519466360
Content-type: text/html; Content-type: text/html;
charset="UTF-8" charset="UTF-8"
Content-transfer-encoding: quoted-printable Content-transfer-encoding: quoted-printable
<html xmlns:o=3D"urn:schemas-microsoft-com:office:office" xmlns:w=3D"urn:schema= <html xmlns:o=3D"urn:schemas-microsoft-com:office:office" xmlns:w=3D"urn:schema=
s-microsoft-com:office:word" xmlns:m=3D"http://schemas.microsoft.com/office/20= s-microsoft-com:office:word" xmlns:m=3D"http://schemas.microsoft.com/office/20=
04/12/omml" xmlns=3D"http://www.w3.org/TR/REC-html40"><head><meta http-equiv=3DC= 04/12/omml" xmlns=3D"http://www.w3.org/TR/REC-html40"><head><meta http-equiv=3DC=
ontent-Type content=3D"text/html; charset=3Dutf-8"><meta name=3DGenerator content=3D= ontent-Type content=3D"text/html; charset=3Dutf-8"><meta name=3DGenerator content=3D=
"Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!-- "Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!--
/* Font Definitions */ /* Font Definitions */
@font-face @font-face
{font-family:"Cambria Math"; {font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;} panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face @font-face
{font-family:Calibri; {font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;} panose-1:2 15 5 2 2 2 4 3 2 4;}
/* Style Definitions */ /* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm; {margin:0cm;
margin-bottom:.0001pt; margin-bottom:.0001pt;
font-size:11.0pt; font-size:11.0pt;
font-family:"Calibri",sans-serif;} font-family:"Calibri",sans-serif;}
a:link, span.MsoHyperlink a:link, span.MsoHyperlink
{mso-style-priority:99; {mso-style-priority:99;
color:blue; color:blue;
text-decoration:underline;} text-decoration:underline;}
span.EmailStyle19 span.EmailStyle19
{mso-style-type:personal-reply; {mso-style-type:personal-reply;
font-family:"Calibri",sans-serif; font-family:"Calibri",sans-serif;
color:windowtext;} color:windowtext;}
.MsoChpDefault .MsoChpDefault
{mso-style-type:export-only; {mso-style-type:export-only;
font-size:10.0pt;} font-size:10.0pt;}
@page WordSection1 @page WordSection1
{size:612.0pt 792.0pt; {size:612.0pt 792.0pt;
margin:72.0pt 72.0pt 72.0pt 72.0pt;} margin:72.0pt 72.0pt 72.0pt 72.0pt;}
div.WordSection1 div.WordSection1
{page:WordSection1;} {page:WordSection1;}
--></style></head><body lang=3Den-ES link=3Dblue vlink=3Dpurple><div class=3DWordSe= --></style></head><body lang=3Den-ES link=3Dblue vlink=3Dpurple><div class=3DWordSe=
ction1><p class=3DMsoNormal><span lang=3DEN-US style=3D'mso-fareast-language:EN-US= ction1><p class=3DMsoNormal><span lang=3DEN-US style=3D'mso-fareast-language:EN-US=
'>Me too, with an attachment<o:p></o:p></span></p><p class=3DMsoNormal><span s= '>Me too, with an attachment<o:p></o:p></span></p><p class=3DMsoNormal><span s=
tyle=3D'mso-fareast-language:EN-US'><o:p>&nbsp;</o:p></span></p><div style=3D'bo= tyle=3D'mso-fareast-language:EN-US'><o:p>&nbsp;</o:p></span></p><div style=3D'bo=
rder:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm'><p class= rder:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm'><p class=
=3DMsoNormal><b><span style=3D'font-size:12.0pt;color:black'>From: </span></b><s= =3DMsoNormal><b><span style=3D'font-size:12.0pt;color:black'>From: </span></b><s=
pan style=3D'font-size:12.0pt;color:black'>Administrator &lt;dlouzan.dummy@gma= pan style=3D'font-size:12.0pt;color:black'>Administrator &lt;dlouzan.dummy@gma=
il.com&gt;<br><b>Reply to: </b>Administrator / htmltest &lt;dlouzan.dummy+c0= il.com&gt;<br><b>Reply to: </b>Administrator / htmltest &lt;dlouzan.dummy+c0=
34670b1623e617e15a3df64223d363@gmail.com&gt;<br><b>Date: </b>Monday, 17 Febr= 34670b1623e617e15a3df64223d363@gmail.com&gt;<br><b>Date: </b>Monday, 17 Febr=
uary 2020 at 22:55<br><b>To: </b>&quot;Louzan Martinez, Diego (ext) (SOP IT = uary 2020 at 22:55<br><b>To: </b>&quot;Louzan Martinez, Diego (ext) (SOP IT =
STG XS)&quot; &lt;diego.louzan.ext@siemens.com&gt;<br><b>Subject: </b>Re: ht= STG XS)&quot; &lt;diego.louzan.ext@siemens.com&gt;<br><b>Subject: </b>Re: ht=
mltest | test issue (#1)<o:p></o:p></span></p></div><div><p class=3DMsoNormal>= mltest | test issue (#1)<o:p></o:p></span></p></div><div><p class=3DMsoNormal>=
<o:p>&nbsp;</o:p></p></div><div><p><span style=3D'color:#777777'><a href=3D"http= <o:p>&nbsp;</o:p></p></div><div><p><span style=3D'color:#777777'><a href=3D"http=
://localhost:3000/root">Administrator</a> commented: <o:p></o:p></span></p><= ://localhost:3000/root">Administrator</a> commented: <o:p></o:p></span></p><=
div><p>I pity the foo !!!<o:p></o:p></p></div></div><div style=3D'margin-top:7= div><p>I pity the foo !!!<o:p></o:p></p></div></div><div style=3D'margin-top:7=
.5pt'><p><span style=3D'font-size:12.0pt;color:#777777'>=E2=80=94 <br>Reply to this = .5pt'><p><span style=3D'font-size:12.0pt;color:#777777'>=E2=80=94 <br>Reply to this =
email directly or <a href=3D"http://localhost:3000/root/htmltest/issues/1#note= email directly or <a href=3D"http://localhost:3000/root/htmltest/issues/1#note=
_1797">view it on GitLab</a>. <br>You're receiving this email because of you= _1797">view it on GitLab</a>. <br>You're receiving this email because of you=
r account on 169.254.169.254. If you'd like to receive fewer emails, you can= r account on 169.254.169.254. If you'd like to receive fewer emails, you can=
<a href=3D"http://localhost:3000/sent_notifications/c034670b1623e617e15a3df64= <a href=3D"http://localhost:3000/sent_notifications/c034670b1623e617e15a3df64=
223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific= 223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific=
ation settings. <o:p></o:p></span></p></div></div></body></html> ation settings. <o:p></o:p></span></p></div></div></body></html>
--B_3664825007_1519466360-- --B_3664825007_1519466360--
--B_3664825007_384940722 --B_3664825007_384940722
Content-type: image/png; name="gitlab_logo.png"; Content-type: image/png; name="gitlab_logo.png";
x-mac-creator="4F50494D"; x-mac-creator="4F50494D";
x-mac-type="504E4766" x-mac-type="504E4766"
Content-disposition: attachment; Content-disposition: attachment;
filename="gitlab_logo.png" filename="gitlab_logo.png"
Content-transfer-encoding: base64 Content-transfer-encoding: base64
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
--B_3664825007_384940722-- --B_3664825007_384940722--
--B_3664825007_1904734766 --B_3664825007_1904734766
Content-type: application/pkcs7-signature; name="smime.p7s" Content-type: application/pkcs7-signature; name="smime.p7s"
Content-transfer-encoding: base64 Content-transfer-encoding: base64
Content-disposition: attachment; Content-disposition: attachment;
filename="smime.p7s" filename="smime.p7s"
MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/ bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/ TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s 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 MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89 dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8 eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6 hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD 7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3 DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ 7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0 ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI 8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs= YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
--B_3664825007_1904734766-- --B_3664825007_1904734766--

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,19 +189,29 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
end end
end end
it 'fails fast for strings with many spaces' do describe 'protects against malicious backtracking' do
content = "coding:" + " " * 50_000 + ";" it 'fails fast for strings with many spaces' do
content = "coding:" + " " * 50_000 + ";"
expect do expect do
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 newlines' do it 'fails fast for strings with many newlines' do
content = "coding:\n" + ";;;" + "\n" * 10_000 + "x" content = "coding:\n" + ";;;" + "\n" * 10_000 + "x"
expect do expect do
Timeout.timeout(3.seconds) { filter(content) } Timeout.timeout(3.seconds) { filter(content) }
end.not_to raise_error end.not_to raise_error
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 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

@ -101,6 +101,7 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning 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,10 +1164,21 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
MARKDOWN MARKDOWN
end end
it { is_expected.to match(%(<section>\nsomething\n</section>)) } describe 'normal regular expression' do
it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } it { is_expected.to match(%(<section>\nsomething\n</section>)) }
it { is_expected.not_to match(%(<section>must be multi-line</section>)) } it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) }
it { expect(subject.match(markdown)[:html]).to eq expected } it { is_expected.not_to match(%(<section>must be multi-line</section>)) }
it { expect(subject.match(markdown)[:html]).to eq expected }
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 end
context 'HTML comment lines' do context 'HTML comment lines' do

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
context 'when primary email is changed' do
before do
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 end
it 'can not be set to an unconfirmed email' do context 'when unconfirmed primary email ' do
unconfirmed = create(:email, user: user) let(:user) { create(:user, :unconfirmed) }
user.commit_email = unconfirmed.email let(:email) { user.email }
expect(user).not_to be_valid it 'can not be set' do
set_email
expect(user).not_to be_valid
end
end end
it 'can not be set to a non-existent email' do context 'when new record' do
user.commit_email = 'non-existent-email@nonexistent.nonexistent' let(:user) { build(:user, :unconfirmed) }
let(:email) { user.email }
expect(user).not_to be_valid it 'can not be set' do
set_email
expect(user).not_to be_valid
end
context 'when skipping confirmation' do
before do
user.skip_confirmation = true
end
it 'can be set' do
set_email
expect(user).to be_valid
end
end
end end
end
it 'can not be set to an invalid email, even if confirmed' do describe 'notification_email' do
confirmed = create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') include_examples 'for user notification, public, and commit emails' do
user.commit_email = confirmed.email subject(:set_email) do
user.notification_email = email
end
end
end
expect(user).not_to be_valid 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}") }
subject { get(url) }
it_behaves_like 'handling get metadata requests', scope: :instance 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 context 'with a duplicate package name in another project' do
subject { get(url) }
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 allow(Ability).to receive(:allowed?).and_call_original
other_group.add_developer(user) allow(Ability).to receive(:allowed?).with(user, :read_note, readable_note).and_return(true)
end allow(Ability).to receive(:allowed?).with(user, :read_note, restricted_note).and_return(restricted_note_access)
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