New upstream version 15.11.11+ds1

This commit is contained in:
Pirate Praveen 2023-07-07 10:43:13 +05:30
parent 169ef31156
commit 6b3dcb98e5
83 changed files with 2187 additions and 641 deletions

View file

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

View file

@ -363,7 +363,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/models/vulnerabilities/finding.rb' - 'ee/app/models/vulnerabilities/finding.rb'
- 'ee/app/presenters/approval_rule_presenter.rb' - 'ee/app/presenters/approval_rule_presenter.rb'
- 'ee/app/presenters/ci/minutes/usage_presenter.rb' - 'ee/app/presenters/ci/minutes/usage_presenter.rb'
- 'ee/app/presenters/merge_request_approver_presenter.rb'
- 'ee/app/serializers/dashboard_operations_project_entity.rb' - 'ee/app/serializers/dashboard_operations_project_entity.rb'
- 'ee/app/serializers/ee/member_user_entity.rb' - 'ee/app/serializers/ee/member_user_entity.rb'
- 'ee/app/services/app_sec/dast/pipelines/find_latest_service.rb' - 'ee/app/services/app_sec/dast/pipelines/find_latest_service.rb'

View file

@ -1069,7 +1069,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/models/approval_merge_request_rule_spec.rb' - 'ee/spec/models/approval_merge_request_rule_spec.rb'
- 'ee/spec/models/approval_state_spec.rb' - 'ee/spec/models/approval_state_spec.rb'
- 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb' - 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb'
- 'ee/spec/models/approval_wrapped_code_owner_rule_spec.rb'
- 'ee/spec/models/approval_wrapped_rule_spec.rb' - 'ee/spec/models/approval_wrapped_rule_spec.rb'
- 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb' - 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb'
- 'ee/spec/models/approvals/wrapped_rule_set_spec.rb' - 'ee/spec/models/approvals/wrapped_rule_set_spec.rb'

View file

@ -2,6 +2,69 @@
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.11.11 (2023-07-04)
### Security (1 change)
- [Add authorization to the subscriptions group controller](gitlab-org/security/gitlab@a6cb78e56a0e6b08ee7bbabd1687fb43a9f8703b) ([merge request](gitlab-org/security/gitlab!3381))
## 15.11.10 (2023-06-28)
### Security (10 changes)
- [Revert 'security-leaked-ci-job-token-permission-15-11' from '15-11'"](gitlab-org/security/gitlab@19f73bf5494d34b43eb8c807f860d545acae0c32) ([merge request](gitlab-org/security/gitlab!3375))
- [Use fully qualified ref when loading code owner file](gitlab-org/security/gitlab@d7ffb4cca68373bff38bd05f0b8afc868cda9e04) ([merge request](gitlab-org/security/gitlab!3354))
- [Maintainer can leak masked webhook secrets by manipulating URL masking](gitlab-org/security/gitlab@3a7ccdac5e41870fdce362c38d0a1d1437906fbd) ([merge request](gitlab-org/security/gitlab!3361))
- [Remove approvals when the only commit gets amended](gitlab-org/security/gitlab@f8a4ad8be7e5fdf752f525ed58b94b1ce625b9a1) ([merge request](gitlab-org/security/gitlab!3368))
- [Fix for fork permissions check in compare controller](gitlab-org/security/gitlab@8edf44b13e55ffe0c912f98134d0341a5a6bcd28) ([merge request](gitlab-org/security/gitlab!3344))
- [Webhook token leaked in Sidekiq logs if log format is 'default'](gitlab-org/security/gitlab@02b58237085930c62ee277c9ebd89a0560f44a98) ([merge request](gitlab-org/security/gitlab!3347))
- [Mitigate epic reference filter ReDOS](gitlab-org/security/gitlab@4c2cd6e5f7c994aca554be37d9ea9e5e114341f1) ([merge request](gitlab-org/security/gitlab!3339))
- [Increasing security for CI_JOB_TOKEN on public and internal projects](gitlab-org/security/gitlab@4f8a00b2499e876df5b65eca921812fbb3215800) ([merge request](gitlab-org/security/gitlab!3319))
- [Sanitize user email addresses in admin confirm user dialog](gitlab-org/security/gitlab@608c8001c349b0a62aae81850de669d3af02ab60) ([merge request](gitlab-org/security/gitlab!3332))
- [Obfuscate email of service desk issue creator in issue REST API](gitlab-org/security/gitlab@a092ebc54cce4492f87f8ed2bf67c31793b0bd0e) ([merge request](gitlab-org/security/gitlab!3316))
## 15.11.9 (2023-06-15)
### Changed (1 change)
- [Make MigrateSharedVulnerabilityIdentifiers use slow iteration](gitlab-org/gitlab@1d91c7b295b22e844b81fb665748c447028525cd) ([merge request](gitlab-org/gitlab!122856)) **GitLab Enterprise Edition**
## 15.11.8 (2023-06-06)
### Fixed (2 changes)
- [Fix memory leak in CI config includes entry](gitlab-org/gitlab@3e367e614c855352295e3bcab25bf5af4ec66bf5) ([merge request](gitlab-org/gitlab!122540))
- [Fix serialization of pull_requests in Bitbucket Server Import](gitlab-org/gitlab@201ad93dfdf8f4acfb6d6eee32e2bd6a4ff68157) ([merge request](gitlab-org/gitlab!122396))
### Security (1 change)
- [Validate description length in labels](gitlab-org/gitlab@2c821ee0823e37a57a6dc049591097232c933713) ([merge request](gitlab-org/gitlab!122697))
### Performance (1 change)
- [LFS: Serve pre-signed URLs in `/lfs/objects/batch`](gitlab-org/gitlab@df3a9655a0e8f0bdab3433cefcbd37acfb8ebcff) ([merge request](gitlab-org/gitlab!122348))
## 15.11.7 (2023-06-05)
### Security (16 changes)
- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@76133e75ad38326bf971e2d913263349781aecbe) ([merge request](gitlab-org/security/gitlab!3200))
- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@e9f9b656b34bb30a7bd66ce82a9d8f6ac43c1ba8) ([merge request](gitlab-org/security/gitlab!3290))
- [Escape the source branch link correctly](gitlab-org/security/gitlab@77ed3e8c2ef51e7bcc89ad1c8c549424a69e3478) ([merge request](gitlab-org/security/gitlab!3288))
- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@98f939c9ba3efd5e51807adbaee189f180131544) ([merge request](gitlab-org/security/gitlab!3283))
- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@29fe6582dd81855cdb263e118459aba370a7c7eb) ([merge request](gitlab-org/security/gitlab!3274))
- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@2b39d58eb21cf2ecc581bc5e1bd4dd48dcfd20bc) ([merge request](gitlab-org/security/gitlab!3262))
- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@8d6e83ff72564f3fa3b24e2040072024b715a073) ([merge request](gitlab-org/security/gitlab!3279))
- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@8ac9a3e3efea62d9b3e6d758ab3f1c43f2354ea5) ([merge request](gitlab-org/security/gitlab!3223))
- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@5d300c3af9c37a7607d795868ae2b4e51b8802c5) ([merge request](gitlab-org/security/gitlab!3257))
- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@f478b7673efd183971e9375be84ad06af641893f) ([merge request](gitlab-org/security/gitlab!3247))
- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@4056d6ccc5b592029ea92ebb90b6e6a66c6eb157) ([merge request](gitlab-org/security/gitlab!3254))
- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@b21208f4c10e8bd4e6754f9bfd2cc5fc96c8be20) ([merge request](gitlab-org/security/gitlab!3267))
- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@c4d0f6256bae18d9bb9f6afc87afeeb3ff971335) ([merge request](gitlab-org/security/gitlab!3285))
- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@fd8298b140dba65ac77ed340a5f78e1fc8032db6) ([merge request](gitlab-org/security/gitlab!3251))
- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@199048eb1c61063409e25d3433e7276faf95709b) ([merge request](gitlab-org/security/gitlab!3230))
- [Validate description length in labels](gitlab-org/security/gitlab@208342903aabd7c4b78c24c0b9b173dfbd62e405) ([merge request](gitlab-org/security/gitlab!3242))
## 15.11.6 (2023-05-24) ## 15.11.6 (2023-05-24)
### Changed (1 change) ### Changed (1 change)

View file

@ -1 +1 @@
15.11.6 15.11.11

View file

@ -1 +1 @@
15.11.6 15.11.11

View file

@ -1 +1 @@
15.11.6 15.11.11

View file

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

View file

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

View file

@ -89,10 +89,14 @@ class Projects::CompareController < Projects::ApplicationController
# target == start_ref == from # target == start_ref == from
def target_project def target_project
strong_memoize(:target_project) do strong_memoize(:target_project) do
next source_project.default_merge_request_target unless compare_params.key?(:from_project_id) target_project =
next source_project if compare_params[:from_project_id].to_i == source_project.id if !compare_params.key?(:from_project_id)
source_project.default_merge_request_target
target_project = target_projects(source_project).find_by_id(compare_params[:from_project_id]) elsif compare_params[:from_project_id].to_i == source_project.id
source_project
else
target_projects(source_project).find_by_id(compare_params[:from_project_id])
end
# Just ignore the field if it points at a non-existent or hidden project # Just ignore the field if it points at a non-existent or hidden project
next source_project unless target_project && can?(current_user, :read_code, target_project) next source_project unless target_project && can?(current_user, :read_code, target_project)

View file

@ -6,6 +6,10 @@ module Repositories
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
# Downloading directly with presigned URLs via batch requests
# require longer expire time.
# The 1h should be enough to download 100 objects.
LFS_DIRECT_BATCH_EXPIRE_IN = 3600.seconds
skip_before_action :lfs_check_access!, only: [:deprecated] skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch] before_action :lfs_check_batch_operation!, only: [:batch]
@ -22,7 +26,11 @@ module Repositories
end end
if download_request? if download_request?
render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE if Feature.enabled?(:lfs_batch_direct_downloads, project)
render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
else
render json: { objects: legacy_download_objects! }, content_type: LfsRequest::CONTENT_TYPE
end
elsif upload_request? elsif upload_request?
render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE
else else
@ -52,11 +60,34 @@ module Repositories
end end
def download_objects! def download_objects!
existing_oids = project.lfs_objects
.for_oids(objects_oids)
.index_by(&:oid)
objects.each do |object|
if lfs_object = existing_oids[object[:oid]]
object[:actions] = download_actions(object, lfs_object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else
object[:error] = {
code: 404,
message: _("Object does not exist on the server or you don't have permissions to access it")
}
end
end
objects
end
def legacy_download_objects!
existing_oids = project.lfs_objects_oids(oids: objects_oids) existing_oids = project.lfs_objects_oids(oids: objects_oids)
objects.each do |object| objects.each do |object|
if existing_oids.include?(object[:oid]) if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object) object[:actions] = proxy_download_actions(object)
if Guest.can?(:download_code, project) if Guest.can?(:download_code, project)
object[:authenticated] = true object[:authenticated] = true
@ -85,7 +116,26 @@ module Repositories
objects objects
end end
def download_actions(object) def download_actions(object, lfs_object)
if lfs_object.file.file_storage? || lfs_object.file.class.proxy_download_enabled?
proxy_download_actions(object)
else
direct_download_actions(lfs_object)
end
end
def direct_download_actions(lfs_object)
{
download: {
href: lfs_object.file.url(
content_type: "application/octet-stream",
expire_at: LFS_DIRECT_BATCH_EXPIRE_IN.since
)
}
}
end
def proxy_download_actions(object)
{ {
download: { download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",

View file

@ -243,13 +243,13 @@ module MergeRequestsHelper
end end
branch = if merge_request.for_fork? branch = if merge_request.for_fork?
_('%{fork_icon} %{source_project_path}:%{source_branch}').html_safe % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } html_escape(_('%{fork_icon} %{source_project_path}:%{source_branch}')) % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch }
else else
merge_request.source_branch merge_request.source_branch
end end
branch_title = if merge_request.for_fork? branch_title = if merge_request.for_fork?
_('%{source_project_path}:%{source_branch}').html_safe % { source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } html_escape(_('%{source_project_path}:%{source_branch}')) % { source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch }
else else
merge_request.source_branch merge_request.source_branch
end end

View file

@ -136,7 +136,7 @@ module UsersHelper
def confirm_user_data(user) def confirm_user_data(user)
message = if user.unconfirmed_email.present? message = if user.unconfirmed_email.present?
_('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email } _('This user has an unconfirmed email address (%{email}). You may force a confirmation.').html_safe % { email: user.unconfirmed_email }
else else
_('This user has an unconfirmed email address. You may force a confirmation.') _('This user has an unconfirmed email address. You may force a confirmation.')
end end

View file

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

View file

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

View file

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

View file

@ -135,6 +135,7 @@ class WebHook < ApplicationRecord
return if url_variables_were.blank? || interpolated_url_was == interpolated_url return if url_variables_were.blank? || interpolated_url_was == interpolated_url
self.url_variables = {} if url_variables_were.keys.intersection(url_variables.keys).any?
self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any? self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any?
end end

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
---
name: lfs_batch_direct_downloads
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122221
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413684
milestone: '16.1'
type: development
group: group::tenant scale
default_enabled: false

View file

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

View file

@ -0,0 +1,299 @@
20211202041233
20211202094944
20211202135508
20211202145237
20211203091642
20211203160952
20211203161149
20211203161840
20211203161942
20211204010826
20211206073851
20211206074547
20211206161271
20211207081708
20211207090503
20211207125331
20211207135331
20211207154413
20211207154414
20211207165508
20211207173510
20211207173511
20211208111425
20211208122200
20211208122201
20211208171402
20211209093636
20211209093828
20211209093923
20211209094222
20211209103048
20211209203820
20211209203821
20211209230042
20211210025754
20211210031721
20211210140000
20211210140629
20211210173137
20211213064821
20211213102111
20211213130324
20211213142344
20211213154259
20211213154704
20211214012507
20211214110307
20211215090620
20211215182006
20211216133107
20211216134134
20211216135651
20211216220939
20211217050753
20211217120000
20211217145923
20211217174331
20211220064757
20211220120402
20211220123956
20211220174504
20211223125921
20211224112937
20211224114539
20211229023654
20211230112517
20211230113031
20220104060049
20220104174445
20220105020514
20220105082217
20220105121325
20220105152547
20220105153149
20220106111958
20220106112043
20220106112085
20220106141756
20220106163326
20220106185033
20220106230629
20220106230712
20220106231518
20220106233459
20220106235626
20220107064845
20220107091629
20220107165036
20220109133006
20220109134455
20220110170953
20220110171049
20220110224913
20220110231420
20220110233155
20220111002756
20220111023852
20220111093534
20220111095006
20220111095007
20220111101421
20220111102314
20220111154950
20220111154951
20220111200254
20220111221516
20220112015940
20220112090556
20220112115413
20220112205111
20220112230642
20220112232037
20220112232605
20220112232723
20220113013319
20220113014438
20220113015830
20220113035519
20220113040447
20220113111440
20220113125401
20220113135449
20220113135924
20220113164801
20220113164901
20220114105525
20220114131950
20220116175851
20220117034056
20220117082611
20220117225936
20220118015633
20220118020026
20220118141950
20220118155846
20220118155847
20220118155848
20220118204039
20220119094023
20220119094503
20220119141407
20220119141736
20220119143130
20220119144253
20220119144458
20220119151221
20220119153706
20220119154442
20220119170426
20220119193130
20220119201340
20220119203119
20220119220620
20220120033115
20220120085655
20220120094340
20220120123700
20220120123800
20220120160625
20220120211831
20220120211832
20220121214752
20220121214753
20220121221651
20220124130028
20220124145019
20220124151456
20220124151949
20220124152824
20220124153233
20220124153234
20220124180704
20220124180705
20220124184338
20220124200927
20220124204046
20220124214131
20220124215857
20220124221521
20220125083520
20220125084127
20220125084348
20220125122228
20220125122640
20220125122725
20220125230538
20220126191624
20220126201752
20220126202654
20220126203421
20220126210021
20220126210022
20220126210657
20220127112243
20220127112412
20220127132200
20220127132201
20220128093756
20220128103042
20220128155251
20220128155814
20220128194722
20220131000000
20220131000001
20220131135725
20220131192643
20220201034731
20220201141705
20220201173212
20220201193033
20220201205300
20220202034409
20220202105733
20220202115350
20220203074916
20220203091304
20220203123333
20220203133652
20220203134942
20220204053655
20220204093120
20220204095121
20220204110725
20220204154220
20220204193000
20220204194347
20220207080758
20220207083129
20220208080921
20220208115439
20220208170445
20220208171826
20220209111007
20220211090920
20220211125954
20220211214605
20220212120735
20220213100000
20220213103859
20220213104531
20220215164709
20220215190020
20220216110023
20220216201949
20220217100008
20220217113058
20220217135229
20220221102333
20220221214928
20220222072536
20220222191845
20220222192524
20220222192525
20220223112304
20220223124428
20220224000000
20220224204415
20220225133705
20220301002101
20220301003502
20220301091503
20220301093434
20220301175104
20220301175426
20220302110724
20220302114046
20220302203410
20220303190555
20220303191047
20220304052335
20220304061631
20220304062107
20220304152729
20220304165107
20220304201847
20220305223212
20220307192534
20220307192610
20220307192645
20220307192725
20220307203458
20220307203459
20220308000205
20220308115219
20220308115502
20220309084838
20220309084954
20220309100648
20220309154855
20220310011530
20220310011613
20220310095341
20220310101118
20220310134207
20220310141349
20220311010352
20220314094841
20220314154235
20220314162342

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,14 @@ module API
end end
expose :moved_to_id expose :moved_to_id
expose :service_desk_reply_to expose :service_desk_reply_to do |issue|
issue.present(
current_user: options[:current_user],
# We need to pass it explicitly to account for the case where `issue`
# is a `WorkItem` which doesn't have a presenter yet.
presenter_class: IssuePresenter
).service_desk_reply_to
end
end end
end end
end end

View file

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

View file

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

View file

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

View file

@ -29,15 +29,19 @@ module Banzai
@references_per_parent[parent_type] ||= begin @references_per_parent[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
prepare_doc_for_scan.to_enum(:scan, regex).each do [filter.object_class.link_reference_pattern, filter.object_class.reference_pattern].each do |pattern|
parent_path = if parent_type == :project next unless pattern
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~) prepare_doc_for_scan.to_enum(:scan, pattern).each do
refs[parent_path] << ident if ident parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~)
refs[parent_path] << ident if ident
end
end end
refs refs
@ -171,15 +175,6 @@ module Banzai
delegate :project, :group, :parent, :parent_type, to: :filter delegate :project, :group, :parent, :parent_type, to: :filter
def regex
strong_memoize(:regex) do
[
filter.object_class.link_reference_pattern,
filter.object_class.reference_pattern
].compact.reduce { |a, b| Regexp.union(a, b) }
end
end
def refs_cache def refs_cache
Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {} Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
end end

View file

@ -80,6 +80,7 @@ module BitbucketServer
state: state, state: state,
title: title, title: title,
source_branch_name: source_branch_name, source_branch_name: source_branch_name,
source_branch_sha: source_branch_sha,
target_branch_name: target_branch_name, target_branch_name: target_branch_name,
target_branch_sha: target_branch_sha target_branch_sha: target_branch_sha
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ module Gitlab
## ##
# Entry that represents a list of include. # Entry that represents a list of include.
# #
class Includes < ::Gitlab::Config::Entry::Node class Includes < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
@ -23,16 +23,8 @@ module Gitlab
end end
end end
def self.aspects def composable_class
super.append -> do Entry::Include
@config = Array.wrap(@config)
@config.each_with_index do |config, i|
@entries[i] = ::Gitlab::Config::Entry::Factory.new(Entry::Include)
.value(config || {})
.create!
end
end
end end
end end
end end

View file

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

View file

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

View file

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

View file

@ -6,7 +6,8 @@ module Gitlab
include Sidekiq::ServerMiddleware include Sidekiq::ServerMiddleware
def call(worker, job, queue) def call(worker, job, queue)
logger.info "arguments: #{Gitlab::Json.dump(job['args'])}" loggable_args = Gitlab::ErrorTracking::Processor::SidekiqProcessor.loggable_arguments(job['args'], job['class'])
logger.info "arguments: #{Gitlab::Json.dump(loggable_args)}"
yield yield
end end
end end

View file

@ -13,6 +13,10 @@ module Gitlab
class UntrustedRegexp class UntrustedRegexp
require_dependency 're2' require_dependency 're2'
# recreate Ruby's \R metacharacter
# https://ruby-doc.org/3.2.2/Regexp.html#class-Regexp-label-Character+Classes
BACKSLASH_R = '(\n|\v|\f|\r|\x{0085}|\x{2028}|\x{2029}|\r\n)'
delegate :===, :source, to: :regexp delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false) def initialize(pattern, multiline: false)

View file

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

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
desc 'db | migration_fix_15_11'
task migration_fix_15_11: [:environment] do
next if Gitlab.com?
only_on = %i[main ci].select { |db| Gitlab::Database.has_database?(db) }
Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |conn, database|
begin
first_migration = conn.execute('SELECT * FROM schema_migrations ORDER BY version ASC LIMIT 1')
rescue ActiveRecord::StatementInvalid
# Uninitialized DB, skip
next
end
next if first_migration.none? # No migrations have been run yet
# If we are affected, the first migration in the schema_migrations table
# will be 20220314184009
next unless first_migration.first['version'] == '20220314184009'
puts "Running 15.11 migration fix for #{database}"
fixes = File.readlines(Rails.root.join('db/15_11_migration_fixes.txt')).map(&:chomp)
conn.transaction do
fixes.each do |version|
conn.execute("INSERT INTO schema_migrations (version) VALUES ('#{version}')")
end
end
end
end
Rake::Task['db:migrate'].enhance(['migration_fix_15_11'])

View file

@ -55,12 +55,13 @@ RSpec.describe Admin::HooksController do
hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' }) hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
hook_params = { hook_params = {
url: 'http://example.com/{baz}?token={token}', url: 'http://example.com/{bar}?token={token}',
enable_ssl_verification: false, enable_ssl_verification: false,
url_variables: [ url_variables: [
{ key: 'token', value: 'some secret value' }, { key: 'token', value: 'some secret value' },
{ key: 'baz', value: 'qux' }, { key: 'baz', value: nil },
{ key: 'foo', value: nil } { key: 'foo', value: nil },
{ key: 'bar', value: 'qux' }
] ]
} }
@ -72,7 +73,7 @@ RSpec.describe Admin::HooksController do
expect(flash[:notice]).to include('was updated') expect(flash[:notice]).to include('was updated')
expect(hook).to have_attributes(hook_params.except(:url_variables)) expect(hook).to have_attributes(hook_params.except(:url_variables))
expect(hook).to have_attributes( expect(hook).to have_attributes(
url_variables: { 'token' => 'some secret value', 'baz' => 'qux' } url_variables: { 'token' => 'some secret value', 'bar' => 'qux' }
) )
end end
end end

View file

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Projects::CompareController do RSpec.describe Projects::CompareController, feature_category: :source_code_management do
include ProjectForksHelper include ProjectForksHelper
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
@ -211,6 +211,36 @@ RSpec.describe Projects::CompareController do
end end
end end
context 'when the target project is the default source but hidden to the user' do
let(:project) { create(:project, :repository, :private) }
let(:from_ref) { 'improve%2Fmore-awesome' }
let(:to_ref) { 'feature' }
let(:whitespace) { nil }
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
from: from_ref,
to: to_ref,
w: whitespace,
page: page,
straight: straight
}
end
it 'does not show the diff' do
allow(controller).to receive(:source_project).and_return(project)
expect(project).to receive(:default_merge_request_target).and_return(private_fork)
show_request
expect(response).to be_successful
expect(assigns(:diffs)).to be_empty
expect(assigns(:commits)).to be_empty
end
end
context 'when the source ref does not exist' do context 'when the source ref does not exist' do
let(:from_project_id) { nil } let(:from_project_id) { nil }
let(:from_ref) { 'non-existent-source-ref' } let(:from_ref) { 'non-existent-source-ref' }

View file

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

View file

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

View file

@ -501,7 +501,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end end
context 'when user has an unconfirmed email', :js do context 'when user has an unconfirmed email', :js do
let(:unconfirmed_user) { create(:user, :unconfirmed) } # Email address contains HTML to ensure email address is displayed in an HTML safe way.
let_it_be(:unconfirmed_email) { "#{generate(:email)}<h2>testing<img/src=http://localhost:8000/test.png>" }
let_it_be(:unconfirmed_user) { create(:user, :unconfirmed, unconfirmed_email: unconfirmed_email) }
where(:path_helper) do where(:path_helper) do
[ [
@ -521,7 +523,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
within_modal do within_modal do
expect(page).to have_content("Confirm user #{unconfirmed_user.name}?") expect(page).to have_content("Confirm user #{unconfirmed_user.name}?")
expect(page).to have_content('This user has an unconfirmed email address. You may force a confirmation.') expect(page).to have_content(
"This user has an unconfirmed email address (#{unconfirmed_email}). You may force a confirmation."
)
click_button 'Confirm user' click_button 'Confirm user'
end end

View file

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

View file

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

View file

@ -153,10 +153,18 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do
end end
describe '#merge_request_source_branch' do describe '#merge_request_source_branch' do
let_it_be(:project) { create(:project) } let(:malicious_branch_name) { 'name<script>test</script>' }
let(:forked_project) { fork_project(project) } let(:project) { create(:project) }
let(:merge_request_forked) { create(:merge_request, source_project: forked_project, target_project: project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:forked_project) { fork_project(project) }
let(:merge_request_forked) do
create(
:merge_request,
source_project: forked_project,
source_branch: malicious_branch_name,
target_project: project
)
end
context 'when merge request is a fork' do context 'when merge request is a fork' do
subject { merge_request_source_branch(merge_request_forked) } subject { merge_request_source_branch(merge_request_forked) }
@ -164,6 +172,10 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do
it 'does show the fork icon' do it 'does show the fork icon' do
expect(subject).to match(/fork/) expect(subject).to match(/fork/)
end end
it 'escapes properly' do
expect(subject).to include(html_escape(malicious_branch_name))
end
end end
context 'when merge request is not a fork' do context 'when merge request is not a fork' do

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::API::Entities::Issue, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let(:issue) { build_stubbed(:issue, project: project) }
let(:current_user) { build_stubbed(:user) }
let(:options) { { current_user: current_user }.merge(option_addons) }
let(:option_addons) { {} }
let(:entity) { described_class.new(issue, options) }
subject(:json) { entity.as_json }
describe '#service_desk_reply_to', feature_category: :service_desk do
# Setting to true (default) doesn't play nice with stubs
let(:option_addons) { { include_subscribed: false } }
let(:issue) { build_stubbed(:issue, project: project, service_desk_reply_to: email) }
let(:email) { 'creator@example.com' }
let(:role) { :developer }
subject { json[:service_desk_reply_to] }
context 'as developer' do
before do
stub_member_access_level(issue.project, developer: current_user)
end
it { is_expected.to eq(email) }
end
context 'as guest' do
before do
stub_member_access_level(issue.project, guest: current_user)
end
it { is_expected.to eq('cr*****@e*****.c**') }
end
context 'without email' do
let(:email) { nil }
specify { expect(json).to have_key(:service_desk_reply_to) }
it { is_expected.to eq(nil) }
end
end
end

View file

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

View file

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

View file

@ -215,6 +215,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
expect(doc.search('.js-render-math').count).to eq(2) expect(doc.search('.js-render-math').count).to eq(2)
end end
it 'protects against malicious backtracking' do
doc = pipeline_filter("$$#{' ' * 1_000_000}$")
expect do
Timeout.timeout(3.seconds) { filter(doc) }
end.not_to raise_error
end
def pipeline_filter(text) def pipeline_filter(text)
context = { project: nil, no_sourcepos: true } context = { project: nil, no_sourcepos: true }
doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) doc = Banzai::Pipeline::PreProcessPipeline.call(text, {})

View file

@ -115,6 +115,7 @@ RSpec.describe BitbucketServer::Representation::PullRequest, feature_category: :
author: "root", author: "root",
description: "Test", description: "Test",
source_branch_name: "refs/heads/root/CODE_OF_CONDUCTmd-1530600625006", source_branch_name: "refs/heads/root/CODE_OF_CONDUCTmd-1530600625006",
source_branch_sha: "074e2b4dddc5b99df1bf9d4a3f66cfc15481fdc8",
target_branch_name: "refs/heads/master", target_branch_name: "refs/heads/master",
target_branch_sha: "839fa9a2d434eb697815b8fcafaecc51accfdbbc", target_branch_sha: "839fa9a2d434eb697815b8fcafaecc51accfdbbc",
title: "Added a new line" title: "Added a new line"

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Includes, feature_category: :pipeline_composition do
subject(:include_entry) { described_class.new(config) }
describe '#initialize' do
let(:config) { 'test.yml' }
it 'does not increase aspects' do
2.times { expect { described_class.new(config) }.not_to change { described_class.aspects.count } }
end
end
end

View file

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

View file

@ -7,6 +7,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let_it_be(:exportable_path) { 'project' } let_it_be(:exportable_path) { 'project' }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:private_project) { create(:project, :private, group: group) }
let_it_be(:private_mr) { create(:merge_request, source_project: private_project, project: private_project) }
let_it_be(:project) { setup_project } let_it_be(:project) { setup_project }
shared_examples 'saves project tree successfully' do shared_examples 'saves project tree successfully' do
@ -118,6 +120,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
@ -492,6 +501,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
create(:milestone, project: project) create(:milestone, project: project)
discussion_note = create(:discussion_note, noteable: issue, project: project) discussion_note = create(:discussion_note, noteable: issue, project: project)
mr_note = create(:note, noteable: merge_request, project: project) mr_note = create(:note, noteable: merge_request, project: project)
create(:system_note, noteable: merge_request, project: project, author: user, note: 'merged')
private_system_note = "mentioned in merge request #{private_mr.to_reference(project)}"
create(:system_note, noteable: merge_request, project: project, author: user, note: private_system_note)
create(:note, noteable: snippet, project: project) create(:note, noteable: snippet, project: project)
create(:note_on_commit, create(:note_on_commit,
author: user, author: user,

View file

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

View file

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

View file

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

View file

@ -258,6 +258,13 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(hook.url_variables).to eq({}) expect(hook.url_variables).to eq({})
end end
it 'resets url variables if url variables are overwritten' do
hook.url_variables = hook.url_variables.merge('abc' => 'baz')
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
it 'does not reset url variables if both url and url variables are changed' do it 'does not reset url variables if both url and url variables are changed' do
hook.url = 'http://example.com/{one}/{two}' hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' } hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }

View file

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

View file

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

View file

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

View file

@ -668,34 +668,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
@ -3461,15 +3543,40 @@ RSpec.describe User, feature_category: :user_profile do
describe '#verified_emails' do describe '#verified_emails' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:confirmed_email) { create(:email, :confirmed, user: user) }
before do
create(:email, user: user)
end
it 'returns only confirmed emails' do it 'returns only confirmed emails' do
email_confirmed = create :email, user: user, confirmed_at: Time.current
create :email, user: user
expect(user.verified_emails).to contain_exactly( expect(user.verified_emails).to contain_exactly(
user.email, user.email,
user.private_commit_email, user.private_commit_email,
email_confirmed.email confirmed_email.email
)
end
it 'does not return primary email when primary email is changed' do
original_email = user.email
user.email = generate(:email)
expect(user.verified_emails).to contain_exactly(
user.private_commit_email,
confirmed_email.email,
original_email
)
end
it 'does not return unsaved primary email even if skip_confirmation is enabled' do
original_email = user.email
user.skip_confirmation = true
user.email = generate(:email)
expect(user.verified_emails).to contain_exactly(
user.private_commit_email,
confirmed_email.email,
original_email
) )
end end
end end

View file

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

View file

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

View file

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

View file

@ -323,6 +323,62 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
it_behaves_like 'process authorization header', renew_authorization: renew_authorization it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end end
context 'when downloading an LFS object that is stored on object storage' do
before do
stub_lfs_object_storage
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
context 'when lfs.object_store.proxy_download=true' do
before do
stub_lfs_object_storage(proxy_download: true)
end
it_behaves_like 'LFS http 200 response'
it 'does return proxied address URL' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
end
end
context 'when "lfs.object_store.proxy_download" is "false"' do
before do
stub_lfs_object_storage(proxy_download: false)
end
it_behaves_like 'LFS http 200 response'
it 'does return direct object storage URL' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/")
expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&")
end
context 'when feature flag "lfs_batch_direct_downloads" is "false"' do
before do
stub_feature_flags(lfs_batch_direct_downloads: false)
end
it_behaves_like 'LFS http 200 response'
it 'does return proxied address URL' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
end
end
end
end
context 'when sending objects=[]' do
let(:body) { download_body([]) }
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 404 }
let(:message) { 'Not found.' }
end
end
end end
context 'when user is authenticated' do context 'when user is authenticated' do

View file

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

View file

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

View file

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

View file

@ -849,3 +849,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

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'rake_helper'
RSpec.describe 'migration_fix_15_11', :reestablished_active_record_base, feature_category: :database do
let(:db) { ApplicationRecord.connection }
let(:target_init_schema) { '20220314184009' }
let(:earlier_init_schema) { '20210101010101' }
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/db/migration_fix_15_11'
Rake::Task.define_task :environment
end
describe 'migration_fix_15_11' do
context 'when fix is needed' do
it 'patches init_schema' do
db.execute('DELETE FROM schema_migrations')
db.execute("INSERT INTO schema_migrations (version) VALUES ('#{target_init_schema}')")
run_rake_task(:migration_fix_15_11)
result = db.execute('SELECT * FROM schema_migrations')
expect(result.count).to eq(300)
end
end
context 'when fix is not needed because no migrations have been run' do
it 'does nothing' do
db.execute('DELETE FROM schema_migrations')
run_rake_task(:migration_fix_15_11)
result = db.execute('SELECT * FROM schema_migrations')
expect(result.count).to eq(0)
end
end
context 'when fix is not needed because DB has not been initialized' do
it 'does nothing' do
db.execute('DROP TABLE schema_migrations')
expect { run_rake_task(:migration_fix_15_11) }.not_to raise_error
end
end
context 'when fix is not needed because there is an earlier init_schema' do
it 'does nothing' do
db.execute('DELETE FROM schema_migrations')
db.execute("INSERT INTO schema_migrations (version) VALUES ('#{earlier_init_schema}')")
run_rake_task(:migration_fix_15_11)
result = db.execute('SELECT * FROM schema_migrations')
expect(result.pluck('version')).to match_array [earlier_init_schema]
end
end
context 'when fix is not needed because the fix has been run already' do
it 'does not affect the schema_migrations table' do
db.execute('DELETE FROM schema_migrations')
db.execute("INSERT INTO schema_migrations (version) VALUES ('#{target_init_schema}')")
run_rake_task(:migration_fix_15_11)
fixed_table = db.execute('SELECT version FROM schema_migrations').pluck('version')
run_rake_task(:migration_fix_15_11)
test_fixed_table = db.execute('SELECT version FROM schema_migrations').pluck('version')
expect(fixed_table).to match_array test_fixed_table
end
end
end
end