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:
commit
f87c4d536a
65 changed files with 1635 additions and 632 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -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)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
15.10.7
|
15.10.8
|
|
@ -1 +1 @@
|
||||||
15.10.7
|
15.10.8
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
15.10.7
|
15.10.8
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
79
lib/gitlab/ci/decompressed_gzip_size_validator.rb
Normal file
79
lib/gitlab/ci/decompressed_gzip_size_validator.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
588
spec/fixtures/emails/valid_reply_signed_smime.eml
vendored
588
spec/fixtures/emails/valid_reply_signed_smime.eml
vendored
|
@ -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> </o:p></span></p><div style=3D'bo=
|
tyle=3D'mso-fareast-language:EN-US'><o:p> </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 <dlouzan.dummy@gma=
|
pan style=3D'font-size:12.0pt;color:black'>Administrator <dlouzan.dummy@gma=
|
||||||
il.com><br><b>Reply to: </b>Administrator / htmltest <dlouzan.dummy+c0=
|
il.com><br><b>Reply to: </b>Administrator / htmltest <dlouzan.dummy+c0=
|
||||||
34670b1623e617e15a3df64223d363@gmail.com><br><b>Date: </b>Monday, 17 Febr=
|
34670b1623e617e15a3df64223d363@gmail.com><br><b>Date: </b>Monday, 17 Febr=
|
||||||
uary 2020 at 22:55<br><b>To: </b>"Louzan Martinez, Diego (ext) (SOP IT =
|
uary 2020 at 22:55<br><b>To: </b>"Louzan Martinez, Diego (ext) (SOP IT =
|
||||||
STG XS)" <diego.louzan.ext@siemens.com><br><b>Subject: </b>Re: ht=
|
STG XS)" <diego.louzan.ext@siemens.com><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> </o:p></p></div><div><p><span style=3D'color:#777777'><a href=3D"http=
|
<o:p> </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/
|
||||||
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
|
||||||
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--
|
||||||
|
|
26
spec/fixtures/markdown.md.erb
vendored
26
spec/fixtures/markdown.md.erb
vendored
|
@ -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]]
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, {})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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*') }
|
||||||
|
|
||||||
|
|
|
@ -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
|
127
spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb
Normal file
127
spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb
Normal 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
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue