New upstream version 15.3.2+ds1

This commit is contained in:
Mohammed Bilal 2022-09-01 14:37:04 +00:00
parent 761ace024e
commit 9072983091
82 changed files with 1496 additions and 635 deletions

View file

@ -726,6 +726,7 @@ Gitlab/NamespacedClass:
- 'app/validators/top_level_group_validator.rb'
- 'app/validators/untrusted_regexp_validator.rb'
- 'app/validators/x509_certificate_credentials_validator.rb'
- 'app/validators/bytesize_validator.rb'
- 'app/workers/admin_email_worker.rb'
- 'app/workers/approve_blocked_pending_approval_users_worker.rb'
- 'app/workers/archive_trace_worker.rb'

View file

@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 15.3.2 (2022-08-30)
### Security (17 changes)
- [No overriding methods for Sawyer class](gitlab-org/security/gitlab@397aa9e269676f4ab3dfba4c3ba8fef131b5b4bd) ([merge request](gitlab-org/security/gitlab!2754))
- [Update Oj to v3.13.21](gitlab-org/security/gitlab@15f86c00b579ad1b4aeedd395f9239e8229c6f8b) ([merge request](gitlab-org/security/gitlab!2730))
- [Prevent long loops when generating suggested branch name](gitlab-org/security/gitlab@1479c9e2a0444794ea274b07e0f59e8a50ced6ee) ([merge request](gitlab-org/security/gitlab!2743))
- [IDOR in Zentao integration issue show page](gitlab-org/security/gitlab@92fdf89045bf294d4ee0338ba3f26c91094a073e) ([merge request](gitlab-org/security/gitlab!2740))
- [Patch VULNDB-255039 (potential Rack cache poisoning)](gitlab-org/security/gitlab@383c926cc8aa4e2c4273556a181e1ddc1b71049f) ([merge request](gitlab-org/security/gitlab!2697))
- [HTML escape the label background color](gitlab-org/security/gitlab@1e43656560fbc13907af72d5d4f696df95d7f49c) ([merge request](gitlab-org/security/gitlab!2719))
- [Sandbox jupyter notebook HTML output](gitlab-org/security/gitlab@3ade5f2fadbb0c15d9e5a14306d0a79136a8f23e) ([merge request](gitlab-org/security/gitlab!2710))
- [Fix unauthorized GFM references in Incident Timeline](gitlab-org/security/gitlab@2e18b59472b5a43921d39433e60038b0f254d123) ([merge request](gitlab-org/security/gitlab!2707))
- [Optimize handling repositories with huge trees](gitlab-org/security/gitlab@4bfaca71c8d8f663242138049cf5639e69326bbb) ([merge request](gitlab-org/security/gitlab!2706))
- [Parse commit trailers without using regexp](gitlab-org/security/gitlab@c15b2cd9b5e572a9bbc7c0c5cb7c9511f1a04ead) ([merge request](gitlab-org/security/gitlab!2699))
- [Check for pathological markdown input](gitlab-org/security/gitlab@2fd5e1133e1acd82cdb524f059b554976cd68f51) ([merge request](gitlab-org/security/gitlab!2733))
- [Replaced smooshpack to fix the vulnerability in LivePreview](gitlab-org/security/gitlab@114637f8f0d9add00914ac3e4562419b0f1b4f63) ([merge request](gitlab-org/security/gitlab!2739))
- [Update package auth for group IP allowlist](gitlab-org/security/gitlab@7e830349a8425dbab65ce92d3e8ebd0afa734381) ([merge request](gitlab-org/security/gitlab!2686))
- [Don't show pipeline status](gitlab-org/security/gitlab@1b5fbb9bcb4dde12a2af075e45407cbc6109494d) ([merge request](gitlab-org/security/gitlab!2712))
- [Sanitize img attributes in Banzai::Filter::ImageLinkFilter](gitlab-org/security/gitlab@22ece3568d6b3aed305ed97aab9fdbb22ca068e8) ([merge request](gitlab-org/security/gitlab!2722))
- [Validate description length for snippets](gitlab-org/security/gitlab@24592d39d7b8956a0e712026e5b988a82d37e771) ([merge request](gitlab-org/security/gitlab!2702))
- [Prevent brute force vuln for Git over HTTP(S) requests](gitlab-org/security/gitlab@fcff307eff525d15e835e65e0e3e3a2395f0b840) ([merge request](gitlab-org/security/gitlab!2716))
## 15.3.1 (2022-08-22)
### Security (1 change)

View file

@ -1 +1 @@
15.3.1
15.3.2

View file

@ -533,7 +533,7 @@ gem 'valid_email', '~> 0.1'
# JSON
gem 'json', '~> 2.5.1'
gem 'json_schemer', '~> 0.2.18'
gem 'oj', '~> 3.13.20'
gem 'oj', '~> 3.13.21'
gem 'multi_json', '~> 1.14.1'
gem 'yajl-ruby', '~> 1.4.3', require: 'yajl'

View file

@ -887,7 +887,7 @@ GEM
plist (~> 3.1)
train-core
wmi-lite (~> 1.0)
oj (3.13.20)
oj (3.13.21)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -1651,7 +1651,7 @@ DEPENDENCIES
oauth2 (~> 2.0)
octokit (~> 4.15)
ohai (~> 16.10)
oj (~> 3.13.20)
oj (~> 3.13.21)
omniauth (~> 1.8)
omniauth-alicloud (~> 1.0.1)
omniauth-atlassian-oauth2 (~> 0.2.0)

View file

@ -1 +1 @@
15.3.1
15.3.2

View file

@ -2,7 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { listen } from 'codesandbox-api';
import { isEmpty, debounce } from 'lodash';
import { Manager } from 'smooshpack';
import { SandpackClient } from '@codesandbox/sandpack-client';
import { mapActions, mapGetters, mapState } from 'vuex';
import {
packageJsonPath,
@ -21,7 +21,7 @@ export default {
},
data() {
return {
manager: {},
client: {},
loading: false,
sandpackReady: false,
};
@ -94,11 +94,11 @@ export default {
this.sandpackReady = false;
eventHub.$off('ide.files.change', this.onFilesChangeCallback);
if (!isEmpty(this.manager)) {
this.manager.listener();
if (!isEmpty(this.client)) {
this.client.cleanup();
}
this.manager = {};
this.client = {};
if (this.listener) {
this.listener();
@ -120,7 +120,7 @@ export default {
return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick())
.then(() => {
this.initManager();
this.initClient();
this.listener = listen((e) => {
switch (e.type) {
@ -136,15 +136,15 @@ export default {
update() {
if (!this.sandpackReady) return;
if (isEmpty(this.manager)) {
if (isEmpty(this.client)) {
this.initPreview();
return;
}
this.manager.updatePreview(this.sandboxOpts);
this.client.updatePreview(this.sandboxOpts);
},
initManager() {
initClient() {
const { codesandboxBundlerUrl: bundlerURL } = this;
const settings = {
@ -155,7 +155,7 @@ export default {
...(bundlerURL ? { bundlerURL } : {}),
};
this.manager = new Manager('#ide-preview', this.sandboxOpts, settings);
this.client = new SandpackClient('#ide-preview', this.sandboxOpts, settings);
},
},
};
@ -164,7 +164,7 @@ export default {
<template>
<div class="preview h-100 w-100 d-flex flex-column gl-bg-white">
<template v-if="showPreview">
<navigator :manager="manager" />
<navigator :client="client" />
<div id="ide-preview"></div>
</template>
<div

View file

@ -8,7 +8,7 @@ export default {
GlLoadingIcon,
},
props: {
manager: {
client: {
type: Object,
required: true,
},
@ -51,7 +51,7 @@ export default {
onUrlChange(e) {
const lastPath = this.path;
this.path = e.url.replace(this.manager.bundlerURL, '') || '/';
this.path = e.url.replace(this.client.bundlerURL, '') || '/';
if (lastPath !== this.path) {
this.currentBrowsingIndex =
@ -79,7 +79,7 @@ export default {
},
visitPath(path) {
// eslint-disable-next-line vue/no-mutating-props
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
this.client.iframe.src = `${this.client.bundlerURL}${path}`;
},
},
};

View file

@ -40,6 +40,13 @@ export default {
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="showOutput" />
<div v-safe-html:[$options.safeHtmlConfig]="rawCode" class="gl-overflow-auto"></div>
<iframe
sandbox
:srcdoc="rawCode"
frameborder="0"
scrolling="no"
width="100%"
class="gl-overflow-auto"
></iframe>
</div>
</template>

View file

@ -36,31 +36,40 @@ class JwtController < ApplicationController
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
if @authentication_result.failed?
render_unauthorized
log_authentication_failed(login, @authentication_result)
render_access_denied
end
end
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
render_access_denied
end
def render_missing_personal_access_token
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: _('HTTP Basic: Access denied\n' \
'You must use a personal access token with \'api\' scope for Git over HTTP.\n' \
'You can generate one at %{profile_personal_access_tokens_url}') % { profile_personal_access_tokens_url: profile_personal_access_tokens_url } }
]
}, status: :unauthorized
def log_authentication_failed(login, result)
log_info = {
message: 'JWT authentication failed',
http_user: login,
remote_ip: request.ip,
auth_service: params[:service],
'auth_result.type': result.type,
'auth_result.actor_type': result.actor&.class
}.merge(::Gitlab::ApplicationContext.current)
Gitlab::AuthLogger.warn(log_info)
end
def render_unauthorized
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
]
}, status: :unauthorized
def render_access_denied
help_page = help_page_url(
'user/profile/account/two_factor_authentication',
anchor: 'troubleshooting'
)
render(
json: { errors: [{
code: 'UNAUTHORIZED',
message: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page)
}] },
status: :unauthorized
)
end
def auth_params

View file

@ -67,9 +67,21 @@ module Repositories
end
send_challenges
render plain: "HTTP Basic: Access denied\n", status: :unauthorized
render_access_denied
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
render_access_denied
end
def render_access_denied
help_page = help_page_url(
'topics/git/troubleshooting_git',
anchor: 'error-on-git-fetch-http-basic-access-denied'
)
render(
plain: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page),
status: :unauthorized
)
end
def basic_auth_provided?
@ -103,13 +115,6 @@ module Repositories
@container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(repository_path)
end
def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: :unauthorized
end
def repository
strong_memoize(:repository) do
repo_type.repository_for(container)

View file

@ -32,7 +32,11 @@ module Resolvers
page_token: cursor
}
tree = repository.tree(args[:ref], args[:path], recursive: args[:recursive], pagination_params: pagination_params)
tree = repository.tree(
args[:ref], args[:path], recursive: args[:recursive],
skip_flat_paths: false,
pagination_params: pagination_params
)
next_cursor = tree.cursor&.next_cursor
Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)

View file

@ -33,11 +33,6 @@ module Types
null: true,
description: 'Text note of the timeline event.'
field :note_html,
GraphQL::Types::String,
null: true,
description: 'HTML note of the timeline event.'
field :promoted_from_note,
Types::Notes::NoteType,
null: true,
@ -67,6 +62,8 @@ module Types
Types::TimeType,
null: false,
description: 'Timestamp when the event updated.'
markdown_field :note_html, null: true, description: 'HTML note of the timeline event.'
end
end
end

View file

@ -171,7 +171,7 @@ module CommitsHelper
ref,
{
merge_request: merge_request&.cache_key,
pipeline_status: commit.status_for(ref)&.cache_key,
pipeline_status: commit.detailed_status_for(ref)&.cache_key,
xhr: request.xhr?,
controller: controller.controller_path,
path: @path # referred to in #link_to_browse_code

View file

@ -247,7 +247,7 @@ module LabelsHelper
class="#{css_class}"
data-container="body"
data-html="true"
#{"style=\"background-color: #{bg_color}\"" if bg_color}
#{"style=\"background-color: #{h bg_color}\"" if bg_color}
>#{ERB::Util.html_escape_once(name)}#{suffix}</span>
HTML
end

View file

@ -69,6 +69,10 @@ module Integrations
}
end
def client_url
api_url.presence || url
end
def self.to_param
name.demodulize.downcase
end

View file

@ -458,7 +458,13 @@ class Issue < ApplicationRecord
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
branch_name_generator = -> (counter) do
suffix = counter > 5 ? SecureRandom.hex(8) : counter
"#{to_branch_name}-#{suffix}"
end
Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end

View file

@ -677,24 +677,24 @@ class Repository
@head_commit ||= commit(self.root_ref)
end
def head_tree
def head_tree(skip_flat_paths: true)
if head_commit
@head_tree ||= Tree.new(self, head_commit.sha, nil)
@head_tree ||= Tree.new(self, head_commit.sha, nil, skip_flat_paths: skip_flat_paths)
end
end
def tree(sha = :head, path = nil, recursive: false, pagination_params: nil)
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil)
if sha == :head
return unless head_commit
if path.nil?
return head_tree
return head_tree(skip_flat_paths: skip_flat_paths)
else
sha = head_commit.sha
end
end
Tree.new(self, sha, path, recursive: recursive, pagination_params: pagination_params)
Tree.new(self, sha, path, recursive: recursive, skip_flat_paths: skip_flat_paths, pagination_params: pagination_params)
end
def blob_at_branch(branch_name, path)

View file

@ -22,6 +22,8 @@ class Snippet < ApplicationRecord
MAX_FILE_COUNT = 10
DESCRIPTION_LENGTH_MAX = 1.megabyte
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content
@ -57,19 +59,10 @@ class Snippet < ApplicationRecord
validates :title, presence: true, length: { maximum: 255 }
validates :file_name,
length: { maximum: 255 }
validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :description_changed?
validates :content, presence: true
validates :content,
length: {
maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
message: -> (_, data) do
current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)
_("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
end
},
if: :content_changed?
validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }, if: :content_changed?
after_create :create_statistics

View file

@ -6,7 +6,7 @@ class Tree
attr_accessor :repository, :sha, :path, :entries, :cursor
def initialize(repository, sha, path = '/', recursive: false, pagination_params: nil)
def initialize(repository, sha, path = '/', recursive: false, skip_flat_paths: true, pagination_params: nil)
path = '/' if path.blank?
@repository = repository
@ -14,7 +14,7 @@ class Tree
@path = path
git_repo = @repository.raw_repository
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, pagination_params)
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, skip_flat_paths, pagination_params)
end
def readme_path

View file

@ -5,12 +5,20 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
presents ::Commit, as: :commit
def status_for(ref)
def detailed_status_for(ref)
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
return unless can?(current_user, :read_commit_status, commit.project)
commit.latest_pipeline(ref)&.detailed_status(current_user)
end
def status_for(ref = nil)
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
return unless can?(current_user, :read_commit_status, commit.project)
commit.status(ref)
end
def any_pipelines?
return false unless can?(current_user, :read_pipeline, commit.project)

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
# BytesizeValidator
#
# Custom validator for verifying that bytesize of a field doesn't exceed the specified limit.
# It is different from Rails length validator because it takes .bytesize into account instead of .size/.length
#
# Example:
#
# class Snippet < ActiveRecord::Base
# validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }
# end
#
# Configuration options:
# * <tt>maximum</tt> - Proc that evaluates the bytesize limit that cannot be exceeded
class BytesizeValidator < ActiveModel::EachValidator
def validate_each(record, attr, value)
size = value.to_s.bytesize
max_size = options[:maximum].call
return if size <= max_size
error_message = format(_('is too long (%{size}). The maximum size is %{max_size}.'), {
size: ActiveSupport::NumberHelper.number_to_human_size(size),
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
})
record.errors.add(attr, error_message)
end
end

View file

@ -14,7 +14,7 @@
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- commit = commit.present(current_user: current_user)
- commit_status = commit.status_for(ref)
- commit_status = commit.detailed_status_for(ref)
- collapsible = local_assigns.fetch(:collapsible, true)
- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
- link = commit_path(project, commit, merge_request: merge_request)

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
if Gem.loaded_specs['rack'].version >= Gem::Version.new("3.0.0")
raise <<~ERR
This patch is unnecessary in Rack versions 3.0.0 or newer.
Please remove this file and the associated spec.
See https://github.com/rack/rack/blob/main/CHANGELOG.md#security (issue #1733)
ERR
end
# Patches a cache poisoning attack vector in Rack by not allowing semicolons
# to delimit query parameters.
# See https://github.com/rack/rack/issues/1732.
#
# Solution is taken from the same issue.
#
# The actual patch is due for release in Rack 3.0.0.
module Rack
class Request
Helpers.module_eval do
# rubocop: disable Naming/MethodName
def GET
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
get_header(RACK_REQUEST_QUERY_HASH)
else
query_hash = parse_query(query_string, '&') # only allow ampersand here
set_header(RACK_REQUEST_QUERY_STRING, query_string)
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
end
end
# rubocop: enable Naming/MethodName
end
end
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
#
# This patch updates SawyerResource class to not allow Ruby methods to be overridden and accessed.
# Any attempt to access a Ruby method will result in an exception.
module SawyerClassPatch
def attr_accessor(*attrs)
attrs.each do |attribute|
class_eval do
# rubocop:disable Gitlab/ModuleWithInstanceVariables
if method_defined?(attribute) || method_defined?("#{attribute}=") || method_defined?("#{attribute}?")
define_method attribute do
raise Sawyer::Error,
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
end
define_method "#{attribute}=" do |value|
raise Sawyer::Error,
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
end
define_method "#{attribute}?" do
raise Sawyer::Error,
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
end
else
define_method attribute do
@attrs[attribute.to_sym]
end
define_method "#{attribute}=" do |value|
@attrs[attribute.to_sym] = value
end
define_method "#{attribute}?" do
!!@attrs[attribute.to_sym]
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
Sawyer::Resource.singleton_class.prepend(SawyerClassPatch)

View file

@ -267,3 +267,8 @@ To resolve this issue, you can update the password expiration by either:
```
The bug was reported [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332455).
## Error on Git fetch: "HTTP Basic: Access Denied"
If you receive an `HTTP Basic: Access denied` error when using Git over HTTP(S),
refer to the [two-factor authentication troubleshooting guide](../../user/profile/account/two_factor_authentication.md#troubleshooting).

View file

@ -299,6 +299,10 @@ hub_docker_quota_check:
## Troubleshooting
## Authentication error: "HTTP Basic: Access Denied"
If you receive an `HTTP Basic: Access denied` error when authenticating against the Dependency Proxy, refer to the [two-factor authentication troubleshooting guide](../../profile/account/two_factor_authentication.md#troubleshooting).
### Dependency Proxy Connection Failure
If a service alias is not set the `docker:20.10.16` image is unable to find the

View file

@ -345,6 +345,11 @@ when a PyPI package is not found in the Package Registry, the request is forward
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
WARNING:
When you use the `--index-url` option, do not specify the port if it is a default
port, such as `80` for a URL starting with `http` or `443` for a URL starting
with `https`.
### Install from the project level
To install the latest version of a package, use the following command:

View file

@ -427,6 +427,39 @@ a GitLab global administrator disable 2FA for your account:
## Troubleshooting
### Error: "HTTP Basic: Access denied. The provided password or token ..."
When making a request, you can receive the following error:
```plaintext
HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal
access token instead of a password.
```
This error occurs in the following scenarios:
- You have 2FA enabled and have attempted to authenticate with a username and
password. For 2FA-enabled users, a [personal access token](../personal_access_tokens.md) (PAT)
must be used instead of a password. To authenticate:
- Git requests over HTTP(S), a PAT with `read_repository` or `write_repository` scope is required.
- [GitLab Container Registry](../../packages/container_registry/index.md#authenticate-with-the-container-registry) requests, a PAT
with `read_registry` or `write_registry` scope is required.
- [Dependency Proxy](../../packages/dependency_proxy/index.md#authenticate-with-the-dependency-proxy) requests, a PAT with
`read_registry` and `write_registry` scopes is required.
- You do not have 2FA enabled and have sent an incorrect username or password
with your request.
- You do not have 2FA enabled but an administrator has enabled the
[enforce 2FA for all users](../../../security/two_factor_authentication.md#enforce-2fa-for-all-users) setting.
- You do not have 2FA enabled, but an administrator has disabled the
[password authentication enabled for Git over HTTP(S)](../../admin_area/settings/sign_in_restrictions.md#password-authentication-enabled)
setting. If LDAP is:
- Configured, an [LDAP password](../../../administration/auth/ldap/index.md)
or a [personal access token](../personal_access_tokens.md)
must be used to authenticate Git requests over HTTP(S).
- Not configured, you must use a [personal access token](../personal_access_tokens.md).
### Error: "invalid pin code"
If you receive an `invalid pin code` error, this can indicate that there is a time sync issue between the authentication
application and the GitLab instance itself. To avoid the time sync issue, enable time synchronization in the device that
generates the codes. For example:

View file

@ -144,7 +144,7 @@ module API
Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user, project: user_project)
end
present commit_detail, with: Entities::CommitDetail, stats: params[:stats]
present commit_detail, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
else
render_api_error!(result[:message], 400)
end
@ -163,7 +163,7 @@ module API
not_found! 'Commit' unless commit
present commit, with: Entities::CommitDetail, stats: params[:stats], current_user: current_user
present commit, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
end
desc 'Get the diff for a specific commit of a project' do

View file

@ -12,7 +12,9 @@ module API
expose :trailers
expose :web_url do |commit, _options|
Gitlab::UrlBuilder.build(commit)
c = commit
c = c.__subject__ if c.is_a?(Gitlab::View::Presenter::Base)
Gitlab::UrlBuilder.build(c)
end
end
end

View file

@ -3,8 +3,10 @@
module API
module Entities
class CommitDetail < Commit
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
include ::API::Helpers::Presentable
expose :stats, using: Entities::CommitStats, if: :include_stats
expose :status_for, as: :status
expose :project_id
expose :last_pipeline do |commit, options|

View file

@ -14,28 +14,12 @@ module API
include Constants
include Gitlab::Utils::StrongMemoize
def unauthorized_user_project
@unauthorized_user_project ||= find_project(params[:id])
end
def unauthorized_user_project!
unauthorized_user_project || not_found!
end
def unauthorized_user_group
@unauthorized_user_group ||= find_group(params[:id])
end
def unauthorized_user_group!
unauthorized_user_group || not_found!
end
def authorized_user_project
@authorized_user_project ||= authorized_project_find!
end
def authorized_project_find!
project = unauthorized_user_project
project = find_project(params[:id])
unless project && can?(current_user, :read_project, project)
return unauthorized_or! { not_found! }

View file

@ -84,6 +84,16 @@ module API
body content
end
def ensure_group!
find_group(params[:id]) || not_found!
find_authorized_group!
end
def ensure_project!
find_project(params[:id]) || not_found!
authorized_user_project
end
end
params do
@ -91,7 +101,7 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
after_validation do
unauthorized_user_group!
ensure_group!
end
namespace ':id/-/packages/pypi' do
@ -101,7 +111,8 @@ module API
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'files/:sha256/*file_identifier' do
group = unauthorized_user_group!
group = find_authorized_group!
authorize_read_package!(group)
filename = "#{params[:file_identifier]}.#{params[:format]}"
package = Packages::Pypi::PackageFinder.new(current_user, group, { filename: filename, sha256: params[:sha256] }).execute
@ -146,7 +157,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
unauthorized_user_project!
ensure_project!
end
namespace ':id/packages/pypi' do
@ -160,7 +171,8 @@ module API
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'files/:sha256/*file_identifier' do
project = unauthorized_user_project!
project = authorized_user_project
authorize_read_package!(project)
filename = "#{params[:file_identifier]}.#{params[:format]}"
package = Packages::Pypi::PackageFinder.new(current_user, project, { filename: filename, sha256: params[:sha256] }).execute

View file

@ -189,7 +189,7 @@ module API
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
if compare
present compare, with: Entities::Compare
present compare, with: Entities::Compare, current_user: current_user
else
not_found!("Ref")
end

View file

@ -123,7 +123,7 @@ module API
get do
verify_search_scope!(resource: nil)
present search, with: entity
present search, with: entity, current_user: current_user
end
end
@ -145,7 +145,7 @@ module API
get ':id/(-/)search' do
verify_search_scope!(resource: user_group)
present search(group_id: user_group.id), with: entity
present search(group_id: user_group.id), with: entity, current_user: current_user
end
end
@ -166,7 +166,7 @@ module API
use :pagination
end
get ':id/(-/)search' do
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity, current_user: current_user
end
end
end

View file

@ -39,7 +39,7 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result])
present commit_detail, with: Entities::CommitDetail
present commit_detail, with: Entities::CommitDetail, current_user: current_user
else
render_api_error!(result[:message], result[:http_status] || 400)
end

View file

@ -17,21 +17,10 @@ module Banzai
include ActionView::Helpers::TagHelper
include AvatarsHelper
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
AUTHOR_REGEXP = /(?<author_name>.+)/.freeze
# Devise.email_regexp wouldn't work here since its designed to match
# against strings that only contains email addresses; the \A and \z
# around the expression will only match if the string being matched
# contains just the email nothing else.
MAIL_REGEXP = /&lt;(?<author_email>[^@\s]+@[^@\s]+)&gt;/.freeze
FILTER_REGEXP = /(?<trailer>^\s*#{TRAILER_REGEXP}\s*#{AUTHOR_REGEXP}\s+#{MAIL_REGEXP}$)/mi.freeze
def call
doc.xpath('descendant-or-self::text()').each do |node|
content = node.to_html
next unless content.match(FILTER_REGEXP)
html = trailer_filter(content)
next if html == content
@ -52,11 +41,24 @@ module Banzai
# Returns a String with all trailer lines replaced with links to GitLab
# users and mailto links to non GitLab users. All links have `data-trailer`
# and `data-user` attributes attached.
#
# The code intentionally avoids using Regex for security and performance
# reasons: https://gitlab.com/gitlab-org/gitlab/-/issues/363734
def trailer_filter(text)
text.gsub(FILTER_REGEXP) do |author_match|
label = $~[:label]
"#{label} #{parse_user($~[:author_name], $~[:author_email], label)}"
end
text.lines.map! do |line|
trailer, rest = line.split(':', 2)
next line unless trailer.downcase.end_with?('-by') && rest.present?
chunks = rest.split
author_email = chunks.pop.delete_prefix('&lt;').delete_suffix('&gt;')
next line unless Devise.email_regexp.match(author_email)
author_name = chunks.join(' ').strip
trailer = "#{trailer.strip}:"
"#{trailer} #{link_to_user_or_email(author_name, author_email, trailer)}\n"
end.join
end
# Find a GitLab user using the supplied email and generate
@ -67,7 +69,7 @@ module Banzai
# trailer - String trailer used in the commit message
#
# Returns a String with a link to the user.
def parse_user(name, email, trailer)
def link_to_user_or_email(name, email, trailer)
link_to_user User.find_by_any_email(email),
name: name,
email: email,

View file

@ -34,17 +34,20 @@ module Banzai
img.remove_attribute('data-diagram-src')
end
link.children = if link_replaces_image
img['alt'] || img['data-src'] || img['src']
else
img.clone
end
link.children = link_replaces_image ? link_children(img) : img.clone
img.replace(link)
end
doc
end
private
def link_children(img)
[img['alt'], img['data-src'], img['src']]
.map { |f| Sanitize.fragment(f).presence }.compact.first || ''
end
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Banzai
module Filter
class PathologicalMarkdownFilter < HTML::Pipeline::TextFilter
# It's not necessary for this to be precise - we just need to detect
# when there are a non-trivial number of unclosed image links.
# So we don't really care about code blocks, etc.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/370428
REGEX = /!\[(?:[^\]])+?!\[/.freeze
DETECTION_MAX = 10
def call
count = 0
@text.scan(REGEX) do |_match|
count += 1
break if count > DETECTION_MAX
end
return @text if count <= DETECTION_MAX
"_Unable to render markdown - too many unclosed markdown image links detected._"
end
end
end
end

View file

@ -5,6 +5,7 @@ module Banzai
class PlainMarkdownPipeline < BasePipeline
def self.filters
FilterArray[
Filter::PathologicalMarkdownFilter,
Filter::MarkdownPreEscapeFilter,
Filter::MarkdownFilter,
Filter::MarkdownPostEscapeFilter

View file

@ -16,9 +16,10 @@ module Gitlab
TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
override :tree_entries
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
if use_rugged?(repository, :rugged_tree_entries)
entries = execute_rugged_call(:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive)
entries = execute_rugged_call(
:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
if pagination_params
paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
@ -60,11 +61,11 @@ module Gitlab
[result, cursor]
end
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive)
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
# This was an optimization to reduce N+1 queries for Gitaly
# (https://gitlab.com/gitlab-org/gitaly/issues/530).
rugged_populate_flat_path(repository, sha, path, entries)
rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
end
end

View file

@ -15,15 +15,16 @@ module Gitlab
# Uses rugged for raw objects
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
def where(repository, sha, path = nil, recursive = false, pagination_params = nil)
def where(repository, sha, path = nil, recursive = false, skip_flat_paths = true, pagination_params = nil)
path = nil if path == '' || path == '/'
tree_entries(repository, sha, path, recursive, pagination_params)
tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params)
end
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
wrapped_gitaly_errors do
repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive, pagination_params)
repository.gitaly_commit_client.tree_entries(
repository, sha, path, recursive, skip_flat_paths, pagination_params)
end
end

View file

@ -5,6 +5,8 @@ module Gitlab
class CommitService
include Gitlab::EncodingHelper
TREE_ENTRIES_DEFAULT_LIMIT = 100_000
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
@ -111,12 +113,16 @@ module Gitlab
nil
end
def tree_entries(repository, revision, path, recursive, pagination_params)
def tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params)
pagination_params ||= {}
pagination_params[:limit] ||= TREE_ENTRIES_DEFAULT_LIMIT
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
path: path.present? ? encode_binary(path) : '.',
recursive: recursive,
skip_flat_paths: skip_flat_paths,
pagination_params: pagination_params
)
request.sort = Gitaly::GetTreeEntriesRequest::SortBy::TREES_FIRST if pagination_params

View file

@ -11,7 +11,7 @@ module Gitlab
# this if the change to the renderer output is a new feature or a
# minor bug fix.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330313
CACHE_COMMONMARK_VERSION = 31
CACHE_COMMONMARK_VERSION = 32
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)

View file

@ -68,6 +68,10 @@ module Gitlab
with { |redis| redis.ttl(cache_key(key)) }
end
def count(key)
with { |redis| redis.scard(cache_key(key)) }
end
private
def with(&blk)

View file

@ -5,6 +5,10 @@ module Gitlab
class Client
Error = Class.new(StandardError)
ConfigError = Class.new(Error)
RequestError = Class.new(Error)
CACHE_MAX_SET_SIZE = 5_000
CACHE_TTL = 1.month.freeze
attr_reader :integration
@ -33,11 +37,21 @@ module Gitlab
end
def fetch_issues(params = {})
get("products/#{zentao_product_xid}/issues", params)
get("products/#{zentao_product_xid}/issues", params).tap do |response|
mark_issues_as_seen_in_product(response['issues'])
end
end
def fetch_issue(issue_id)
raise Gitlab::Zentao::Client::Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
raise Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
# Only return issues that are associated with the product configured in
# the integration. Due to a lack of available data in the ZenTao APIs, we
# can only determine if an issue belongs to a product if the issue was
# previously returned in the `#fetch_issues` call.
#
# See https://gitlab.com/gitlab-org/gitlab/-/issues/360372#note_1016963713
raise RequestError unless issue_seen_in_product?(issue_id)
get("issues/#{issue_id}")
end
@ -52,17 +66,15 @@ module Gitlab
options = { headers: headers, query: params }
response = Gitlab::HTTP.get(url(path), options)
raise Gitlab::Zentao::Client::Error, 'request error' unless response.success?
raise RequestError unless response.success?
Gitlab::Json.parse(response.body)
rescue JSON::ParserError
raise Gitlab::Zentao::Client::Error, 'invalid response format'
raise Error, 'invalid response format'
end
def url(path)
host = integration.api_url.presence || integration.url
URI.parse(Gitlab::Utils.append_path(host, "api.php/v1/#{path}"))
URI.parse(Gitlab::Utils.append_path(integration.client_url, "api.php/v1/#{path}"))
end
def headers
@ -75,6 +87,30 @@ module Gitlab
def zentao_product_xid
integration.zentao_product_xid
end
def issue_ids_cache_key
@issue_ids_cache_key ||= [
:zentao_product_issues,
OpenSSL::Digest::SHA256.hexdigest(integration.client_url),
zentao_product_xid
].join(':')
end
def issue_ids_cache
@issue_ids_cache ||= ::Gitlab::SetCache.new(expires_in: CACHE_TTL)
end
def mark_issues_as_seen_in_product(issues)
return unless issues && issue_ids_cache.count(issue_ids_cache_key) < CACHE_MAX_SET_SIZE
ids = issues.map { _1['id'] }
issue_ids_cache.write(issue_ids_cache_key, ids)
end
def issue_seen_in_product?(id)
issue_ids_cache.include?(issue_ids_cache_key, id)
end
end
end
end

View file

@ -19065,7 +19065,7 @@ msgstr ""
msgid "HTTP Archive (HAR)"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
msgid "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"
msgstr ""
msgid "Harbor Registry"
@ -46573,6 +46573,9 @@ msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}."
msgstr ""
msgid "is too long (%{size}). The maximum size is %{max_size}."
msgstr ""
msgid "is too long (maximum is %{count} characters)"
msgstr ""

View file

@ -49,6 +49,7 @@
"@apollo/client": "^3.5.10",
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"@codesandbox/sandpack-client": "^1.2.2",
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "3.1.0",
@ -164,7 +165,6 @@
"remark-rehype": "^10.1.0",
"scrollparent": "^2.0.1",
"select2": "3.5.2-browserify",
"smooshpack": "^0.0.62",
"sortablejs": "^1.10.2",
"string-hash": "1.1.3",
"style-loader": "^2.0.0",

View file

@ -16,4 +16,4 @@ install:
script:
- "pip install <%= package.name %> --no-deps --index-url <%= uri.scheme %>://<%= personal_access_token %>:<%= personal_access_token %>@<%= gitlab_host_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host <%= gitlab_host_with_port %>"
tags:
- runner-for-<%= project.name %>
- runner-for-<%= project.name %>

View file

@ -30,9 +30,16 @@ module QA
end
let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
let(:personal_access_token) { use_ci_variable(name: 'PERSONAL_ACCESS_TOKEN', value: Runtime::Env.personal_access_token, project: project) }
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
let(:gitlab_host_with_port) do
# Don't specify port if it is a standard one
if uri.port == 80 || uri.port == 443
uri.host
else
"#{uri.host}:#{uri.port}"
end
end
before do
Flow::Login.sign_in

View file

@ -1,294 +1,294 @@
User-Agent: Microsoft-MacOutlook/10.22.0.200209
Date: Mon, 17 Feb 2020 22:56:47 +0100
Subject: Re: htmltest | test issue (#1)
From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)"
<diego.louzan.ext@siemens.com>
To: Administrator / htmltest
<dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com>
Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com>
Thread-Topic: htmltest | test issue (#1)
References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254>
<issue_451@169.254.169.254>
<note_1797@169.254.169.254>
In-Reply-To: <note_1797@169.254.169.254>
Content-type: multipart/signed;
protocol="application/pkcs7-signature";
micalg=sha256;
boundary="B_3664825007_1904734766"
MIME-Version: 1.0
--B_3664825007_1904734766
Content-type: multipart/mixed;
boundary="B_3664825007_384940722"
--B_3664825007_384940722
Content-type: multipart/alternative;
boundary="B_3664825007_1519466360"
--B_3664825007_1519466360
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Me too, with an attachment
=20
From: Administrator <dlouzan.dummy@gmail.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64=
223d363@gmail.com>
Date: Monday, 17 February 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen=
s.com>
Subject: Re: htmltest | test issue (#1)
=20
Administrator commented:=20
I pity the foo !!!
=E2=80=94=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'd like to receive fewer emails, you can unsubscribe from this thread or =
adjust your notification settings.=20
--B_3664825007_1519466360
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<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=
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=
"Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!--
/* Font Definitions */
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm;
margin-bottom:.0001pt;
font-size:11.0pt;
font-family:"Calibri",sans-serif;}
a:link, span.MsoHyperlink
{mso-style-priority:99;
color:blue;
text-decoration:underline;}
span.EmailStyle19
{mso-style-type:personal-reply;
font-family:"Calibri",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;
font-size:10.0pt;}
@page WordSection1
{size:612.0pt 792.0pt;
margin:72.0pt 72.0pt 72.0pt 72.0pt;}
div.WordSection1
{page:WordSection1;}
--></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=
'>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=
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=
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=
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 =
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>=
<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><=
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 =
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=
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=
223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific=
ation settings. <o:p></o:p></span></p></div></div></body></html>
--B_3664825007_1519466360--
--B_3664825007_384940722
Content-type: image/png; name="gitlab_logo.png";
x-mac-creator="4F50494D";
x-mac-type="504E4766"
Content-disposition: attachment;
filename="gitlab_logo.png"
Content-transfer-encoding: base64
iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAABnRSTlMA/wD/AP83WBt9AAAN
1UlEQVR4AexcZXPjSBTcXxOTvMy7xxfGZWaGaJmZmZmZmZmZmdnMzB7JNwv1qs6VOJY0tuWU
p/rz5PW0q0f99JQakcxK6eItQGZlBMgIkFkZATICZFZGgIwAmZURICMAshitiybrexXblk5D
NnOk2i3G6bCvmYcJWuaMCevVohPAsWGx6h/Zd/wrd2xbWf0EcB3YqsqmfnK0LZseYZCIBEBW
E/5p4Mp+wtCvJWO3Vqufv8dtHNoZCOo6ZYd1ahEJ4LtzRZ1fC+pTF9T1P7hZnQQIvHqiKW0I
BFU5lPfiCREJYFs5C4r7Cfu6BdVJAOeutVEErfPGRRhGFAIgu1Xft0VUfYaBbRmXI1ItFuvz
Gkd0jyKo65oXNupEIYD//g11QZ2o+tRF9QJP7lUPAYJvX2haNIkmmKv0Xj0rCgHsa+dDWRgA
x+al1eT5Z9+mCglaF02KsGyKBWCcdsOA1hXWZ6A7MB5X2vtPwG8a07tCgvoehchsSLEA/sd3
sNtUWJ+mpEHgxaN0FyD08Y2mVbMKCarzavluXkyxAI5NS3AplcG5fVXa+8+h7TEI4kSWSgEY
t9NQ3j5GfcZhXRivJ439JxgwT+gfg6C+dymymlMmQOD5Q01xgxj1acoaBV8/S2P/+fJe2+b3
GATV+bV9d6+lTADc88FFxIZz9/r0FcB9fE+VBO2r56RGAMYL7ZFYMI3qwfp9aek/oZB5Snks
dtD4cthSIEDw1VNNaaMq69O0bBp8/yot/Uf1Wdv+zyoJqgvr+h/eSoEAzl3roIjYcB3Yko4C
eE4fxK31eAja1y9MogDQHhnZPU4BTGP74jiTZv6DwpYZw+MkaBgEja9kCRB89xLaI1VC27p5
6NPb9BIgrP2m6/hP1eyg8fX0XlIFcO3fHE9lAPeRnWnmP+ePqbIV8RN0bF6WHAGgPdKHkwDm
iQPZUDB9XoAhy5zRnAga6Y78Gl81SLVHYkPb9o/Q149p4z96ja5LDieCmpKG0PhKuACuwzvi
rwze1LtP7EsXAbyXT6lylFw5OnesTrQA0B4ZwLU4DPPUIWw4lA4PQIx1wQQeBI3Du7JeT8IF
CH35AO0RTtC2/yus/hIR/UImva5bPg+CmrLGwTfPEi6A+/heiCfckK3wnD0sfgF818+rc2ty
ogZw7tmQWAHYMG6P0FzLAlhmjoggJG7/YW1LpvImaBrVk2vjqwb39shfvOvTdfo3rFOJ2n8s
Jn3PYn7soPGVQAE8Zw6B//BBNp5nOi5q/7l9GSbM+AFPMCZKAGiPCIF13liYZxLhsq2YJZCg
aVxfNhggLgC0R/7lXxzMMxm0IvUfu0Xfp0wAO2h8vUuIAJ4L0B7hD3UOnmc6I04BYMJMINxH
d5EVANojY/jWRH6eifyCCTPBME8aBI0vYgKEDbg9kkukPphnEtWCCTPhgMYXSQG8V05De0Qg
1Hk1YZ5JFAsmzArrCWUHja+T+4kKwLLWhRPJFAfzTCJbjo2LCRI0T8ONrzAJAaA90r2AYH36
3iUwz5TiBRNmg9sTJKjt8HdY/ZWYAL4bvNsjMeaZropHgMDzB5ri+gQJQuOLiACsbSm0R4jB
vmqOiPxn6wriBC2zRkYQIiAAfIBHFnr4kE9kH+CRAIcP+Wpw/QCPBGCe6aYYP8AjBfiQj78A
0B75W5YIiORDPufOtQkiaJkLH/LxFYB1W22j2xjL5MaWSsIoU9iGt/LfuYQbAKnEvau2cZ0S
RNBKFzE2vTABtNfDKxqEh8jC5VLyoBWmdnVVubXUeamBKremsXXdULkiIezwoS2uy349I0gA
5uFctD0LzaFQuQSVZxEGneXoitM1vGBIAeydlYgGakQxk0Lbspg7EyIsy1eAgJ051RLtyEJb
ZWiyAg0mX6W/P6XJU6Tq9NW5Cl9fCtGkeeGDmqBAW+Tfj+5YXsRr4CkAq7+N9tT+vsvOLLRB
gcbIiWsQLpdhu1T9nRoBDKXK0GAZ+d/+KBlap8CH9v3odilY1QWeAjBPFuEtMH5psJJCw6Sk
XUji6FozVS5k61STvP8MlaLlFNopgaNj7k3lJUDQyZxp82MLgAQtpAhXTKfMhdQ5Ci95/5Gg
eRTaIf3fuZ0oivhMnAVgjffR3rq/tgBsl6EZFHEXMpSlwIX0JeT8B6x/Kr54ZdGHtlvJaq5w
FoB5tvx/u4ARbZaj8UQvZFpi71wzBf7TkZD/wOmPlaONv6w/CsyDWRwFCLmZcx2iNwIN1lJo
pIygC/n6UfiBJNn+04eo/wyXodUUnH4UmFOlEb+VgwCs6THaVz96IwC+YZZSaCixCzmUdBfS
F2P/kRM7/SEStBgu3oqwpxaru8lBAObFmkr2AkghnaWjC1k7EPQfyffMtV0a+8SYR/PjFiDs
ZS50jb3dr3Q2RfBlAC7Ul8K2kCT/yVZ4euMATMj6J/7KXLHBnG6Fg21cArCW52h/w9jbEU9n
+IFEX6pMjgC6YmVwkJxQ5pKj9XDxxsSe2qzhbnwCvNpY9XagwSoK3z9EXMjWMSku9LfM2h78
h3Dmig3myZI4BAj7mYs9q9yLfDqjs7x9kuFC6my5pxcJ/6GjM1eVYM62iwRdVQjA2t6gA405
CEAuneHHEhyOEu4/RRQR/4HMxQF767LGh1UJ8GY7t00hnU0QfCHTEmuiXQi/pWoH/iMsc20C
6+cA5vmqmAIgP3OlP8dNIZ0phKYzOsvTR6nmMP/La2ZNuP+MgMzFGcz5zpGQq1IBWOsrdLA5
530hnS0TkM7AhYqVCfSfQuw/ClKZiw/2N2QN9ysVgHm5Hu2EW4UHpGiusHRGS3BEgkhM3H/M
bbH/SAVlrlmQuXiCebygcgHOdeSxI5l0Bi7UG7uQPEH+4+oJ/kMoc/HAiaJKBYh+/uF3GWwU
lM7wIwp+UEmEANoCKjBQQThz8cBuZeUCHPqdx46E0xktsbQj6kLgP214+Q9krhX8rT/qYbRy
C7oxXOjukM4W8U1ndBZ+UFFly8n7Tw++/oOJzIfMJRTMpd6VCsBanqFjuWQ0wDfVTIq/CxVS
IvKfaZC5BOPwn6z+Tswgpr+DTpaS+WNb+KYzWkrWhfBWptY18bAUn4t3HM5cckHWDzieD+8m
Y7ajXd+Ym6PQLorAZbCOYzoDF+qpxKZB0H+c3fEFwCtzraEInP4uOXOtnHV8iPuVZNiLexI8
QhmpdBYcqNCScyFNPhUYoOCeuaRoCYmLd39j9uW6SMjNdS6IZY0PfiQDgRVI0Tzu6YyWmtsI
diHwn1ZK7v4jQbMFZS54D/P9ZSTL8B1P9xmZBzN+zcfxxjbZ997hYG4u5OpByoXkzm5KRHO0
/kmCM9du5ffBUI9W8CdKTJD9fBQd/VdoOhvLLZ0FsAsVUAT8J4/y9+foP6MFZ67Df7Dv90aQ
n8AHGvCegLncD+2U8ddgNdd0JjW3FuxCf+PZU+w/XP7uMGGZa6eUudCNNT9NwL+rCTq+T2vt
ayAonQ2RcHCh7sJdSI5nTxGd8MwFKff79IPfkrB/WcYiVn0ZnSxJTjrDjy7afEqY/yjw7Cmi
k5K5juex/7V3Dz5yhVEUwP+cce2GjWu7cW3btm03qm27QRXVtt2ZbO8op/r2vp7qS+a+uHHP
5r7z252ze2N7UUrZZxMB0FBw6GxQUJ1JdXlEXSHcn3oB7g/MFSPN5a75fyEAQGG5QIHUWe9I
wCskBYa4Qrg/rfADSNZces1Poeb/swAoKEBnM4Lq7H372B32Ct2RAUxb3B/KXHzN/wcBcFCA
zor92sQVIic01eTzprg/pLn0mn/Hgz/mKVC4moECobMgV4gd8snnTfWM5fTL/G1ZlK75HgTA
QUGu7eJAOhNG6RMaboDXKWOuhTAXUfM9CICGAnTGD/m4AR7MNQunn6j5HgTAQgEv5CnQGTHk
IwZ4MNfE+C80iE2o+Z4GgBTSUOgFKKg6G41vl5JDPmKANyKAuVDzO6HmexAAAQVSZxjy1cMV
ogd4OP0yc1uimgs1Hx9n8zIAHgp4GSwQnUWZCQ0xwBNzzYO5yJrvfwCAwmmBQklGZ8SQDwM8
t7mm4cVL1HzvA+ChEE5OcOoMc2JqgAdzjcU3O4ma70EAPBQup/a3cUEBOhse168QMcCDuSLB
aj7xu329CICHAnTWHzrThnz6AA//+30VcxE1388AeChAZz0jxJAPAzynuYia738AxPPqRgYK
sWJ1Fv7xCgmvlAHMtwM8mGsSzKXW/AIIQIUCdKYP+fQBnkzYVkQcNb8ian5hBQAoNMPX5nc6
Gwyd6UM+DPB0cyk1vwACUKAAnfWJ6kO+YgZ4vcRcePHqNb9gAlCggJfBTPyaLveQzzHA6wZz
OWu+BaBAATpThnx3McBzmctR8y0ABQrQmXvIhwGe21zrSqfOjUfNtwB0KEBnUegsN+SLOQd4
MJde8y0ARwqAQj6DudBZZsiXcA5gekSSs2EureZbAAoUquKFPDWns++HfBjgwVyo+RfmoeZb
ADQUcjobk9HZN0M+DPBgLtT8I0TNtwDcUFiW0dm3Qz7cn4E5c2Vq/gCm5lsAChSgs+wVwgAP
5krX/LV8zbcAFCisjiRnxpI9wrkhX3qAlxCsibnYD+1YAAQUJkQ/dozL8ZEBzIf28eTYaHJt
Ga7mWwAEFPalNtdNDo89bphIfwBdzLWhBlnzLQD+JwoH+7/qVvFlpwqpPT34mm8B8M/n15+P
Lf90cGHRpxf4RwvAHt8DsMcCsADssQAsAHssAAvAni8AV5380akCdgAAAABJRU5ErkJggg==
--B_3664825007_384940722--
--B_3664825007_1904734766
Content-type: application/pkcs7-signature; name="smime.p7s"
Content-transfer-encoding: base64
Content-disposition: attachment;
filename="smime.p7s"
MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s
b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G
A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1
R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ
OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/
0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m
D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2
NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940
tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl
gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5
4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM
tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px
wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA
MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ
MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg
VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw
HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO
MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
--B_3664825007_1904734766--
User-Agent: Microsoft-MacOutlook/10.22.0.200209
Date: Mon, 17 Feb 2020 22:56:47 +0100
Subject: Re: htmltest | test issue (#1)
From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)"
<diego.louzan.ext@siemens.com>
To: Administrator / htmltest
<dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com>
Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com>
Thread-Topic: htmltest | test issue (#1)
References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254>
<issue_451@169.254.169.254>
<note_1797@169.254.169.254>
In-Reply-To: <note_1797@169.254.169.254>
Content-type: multipart/signed;
protocol="application/pkcs7-signature";
micalg=sha256;
boundary="B_3664825007_1904734766"
MIME-Version: 1.0
--B_3664825007_1904734766
Content-type: multipart/mixed;
boundary="B_3664825007_384940722"
--B_3664825007_384940722
Content-type: multipart/alternative;
boundary="B_3664825007_1519466360"
--B_3664825007_1519466360
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Me too, with an attachment
=20
From: Administrator <dlouzan.dummy@gmail.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64=
223d363@gmail.com>
Date: Monday, 17 February 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen=
s.com>
Subject: Re: htmltest | test issue (#1)
=20
Administrator commented:=20
I pity the foo !!!
=E2=80=94=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'd like to receive fewer emails, you can unsubscribe from this thread or =
adjust your notification settings.=20
--B_3664825007_1519466360
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<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=
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=
"Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!--
/* Font Definitions */
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm;
margin-bottom:.0001pt;
font-size:11.0pt;
font-family:"Calibri",sans-serif;}
a:link, span.MsoHyperlink
{mso-style-priority:99;
color:blue;
text-decoration:underline;}
span.EmailStyle19
{mso-style-type:personal-reply;
font-family:"Calibri",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;
font-size:10.0pt;}
@page WordSection1
{size:612.0pt 792.0pt;
margin:72.0pt 72.0pt 72.0pt 72.0pt;}
div.WordSection1
{page:WordSection1;}
--></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=
'>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=
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=
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=
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 =
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>=
<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><=
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 =
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=
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=
223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific=
ation settings. <o:p></o:p></span></p></div></div></body></html>
--B_3664825007_1519466360--
--B_3664825007_384940722
Content-type: image/png; name="gitlab_logo.png";
x-mac-creator="4F50494D";
x-mac-type="504E4766"
Content-disposition: attachment;
filename="gitlab_logo.png"
Content-transfer-encoding: base64
iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAABnRSTlMA/wD/AP83WBt9AAAN
1UlEQVR4AexcZXPjSBTcXxOTvMy7xxfGZWaGaJmZmZmZmZmZmdnMzB7JNwv1qs6VOJY0tuWU
p/rz5PW0q0f99JQakcxK6eItQGZlBMgIkFkZATICZFZGgIwAmZURICMAshitiybrexXblk5D
NnOk2i3G6bCvmYcJWuaMCevVohPAsWGx6h/Zd/wrd2xbWf0EcB3YqsqmfnK0LZseYZCIBEBW
E/5p4Mp+wtCvJWO3Vqufv8dtHNoZCOo6ZYd1ahEJ4LtzRZ1fC+pTF9T1P7hZnQQIvHqiKW0I
BFU5lPfiCREJYFs5C4r7Cfu6BdVJAOeutVEErfPGRRhGFAIgu1Xft0VUfYaBbRmXI1ItFuvz
Gkd0jyKo65oXNupEIYD//g11QZ2o+tRF9QJP7lUPAYJvX2haNIkmmKv0Xj0rCgHsa+dDWRgA
x+al1eT5Z9+mCglaF02KsGyKBWCcdsOA1hXWZ6A7MB5X2vtPwG8a07tCgvoehchsSLEA/sd3
sNtUWJ+mpEHgxaN0FyD08Y2mVbMKCarzavluXkyxAI5NS3AplcG5fVXa+8+h7TEI4kSWSgEY
t9NQ3j5GfcZhXRivJ439JxgwT+gfg6C+dymymlMmQOD5Q01xgxj1acoaBV8/S2P/+fJe2+b3
GATV+bV9d6+lTADc88FFxIZz9/r0FcB9fE+VBO2r56RGAMYL7ZFYMI3qwfp9aek/oZB5Snks
dtD4cthSIEDw1VNNaaMq69O0bBp8/yot/Uf1Wdv+zyoJqgvr+h/eSoEAzl3roIjYcB3Yko4C
eE4fxK31eAja1y9MogDQHhnZPU4BTGP74jiTZv6DwpYZw+MkaBgEja9kCRB89xLaI1VC27p5
6NPb9BIgrP2m6/hP1eyg8fX0XlIFcO3fHE9lAPeRnWnmP+ePqbIV8RN0bF6WHAGgPdKHkwDm
iQPZUDB9XoAhy5zRnAga6Y78Gl81SLVHYkPb9o/Q149p4z96ja5LDieCmpKG0PhKuACuwzvi
rwze1LtP7EsXAbyXT6lylFw5OnesTrQA0B4ZwLU4DPPUIWw4lA4PQIx1wQQeBI3Du7JeT8IF
CH35AO0RTtC2/yus/hIR/UImva5bPg+CmrLGwTfPEi6A+/heiCfckK3wnD0sfgF818+rc2ty
ogZw7tmQWAHYMG6P0FzLAlhmjoggJG7/YW1LpvImaBrVk2vjqwb39shfvOvTdfo3rFOJ2n8s
Jn3PYn7soPGVQAE8Zw6B//BBNp5nOi5q/7l9GSbM+AFPMCZKAGiPCIF13liYZxLhsq2YJZCg
aVxfNhggLgC0R/7lXxzMMxm0IvUfu0Xfp0wAO2h8vUuIAJ4L0B7hD3UOnmc6I04BYMJMINxH
d5EVANojY/jWRH6eifyCCTPBME8aBI0vYgKEDbg9kkukPphnEtWCCTPhgMYXSQG8V05De0Qg
1Hk1YZ5JFAsmzArrCWUHja+T+4kKwLLWhRPJFAfzTCJbjo2LCRI0T8ONrzAJAaA90r2AYH36
3iUwz5TiBRNmg9sTJKjt8HdY/ZWYAL4bvNsjMeaZropHgMDzB5ri+gQJQuOLiACsbSm0R4jB
vmqOiPxn6wriBC2zRkYQIiAAfIBHFnr4kE9kH+CRAIcP+Wpw/QCPBGCe6aYYP8AjBfiQj78A
0B75W5YIiORDPufOtQkiaJkLH/LxFYB1W22j2xjL5MaWSsIoU9iGt/LfuYQbAKnEvau2cZ0S
RNBKFzE2vTABtNfDKxqEh8jC5VLyoBWmdnVVubXUeamBKremsXXdULkiIezwoS2uy349I0gA
5uFctD0LzaFQuQSVZxEGneXoitM1vGBIAeydlYgGakQxk0Lbspg7EyIsy1eAgJ051RLtyEJb
ZWiyAg0mX6W/P6XJU6Tq9NW5Cl9fCtGkeeGDmqBAW+Tfj+5YXsRr4CkAq7+N9tT+vsvOLLRB
gcbIiWsQLpdhu1T9nRoBDKXK0GAZ+d/+KBlap8CH9v3odilY1QWeAjBPFuEtMH5psJJCw6Sk
XUji6FozVS5k61STvP8MlaLlFNopgaNj7k3lJUDQyZxp82MLgAQtpAhXTKfMhdQ5Ci95/5Gg
eRTaIf3fuZ0oivhMnAVgjffR3rq/tgBsl6EZFHEXMpSlwIX0JeT8B6x/Kr54ZdGHtlvJaq5w
FoB5tvx/u4ARbZaj8UQvZFpi71wzBf7TkZD/wOmPlaONv6w/CsyDWRwFCLmZcx2iNwIN1lJo
pIygC/n6UfiBJNn+04eo/wyXodUUnH4UmFOlEb+VgwCs6THaVz96IwC+YZZSaCixCzmUdBfS
F2P/kRM7/SEStBgu3oqwpxaru8lBAObFmkr2AkghnaWjC1k7EPQfyffMtV0a+8SYR/PjFiDs
ZS50jb3dr3Q2RfBlAC7Ul8K2kCT/yVZ4euMATMj6J/7KXLHBnG6Fg21cArCW52h/w9jbEU9n
+IFEX6pMjgC6YmVwkJxQ5pKj9XDxxsSe2qzhbnwCvNpY9XagwSoK3z9EXMjWMSku9LfM2h78
h3Dmig3myZI4BAj7mYs9q9yLfDqjs7x9kuFC6my5pxcJ/6GjM1eVYM62iwRdVQjA2t6gA405
CEAuneHHEhyOEu4/RRQR/4HMxQF767LGh1UJ8GY7t00hnU0QfCHTEmuiXQi/pWoH/iMsc20C
6+cA5vmqmAIgP3OlP8dNIZ0phKYzOsvTR6nmMP/La2ZNuP+MgMzFGcz5zpGQq1IBWOsrdLA5
530hnS0TkM7AhYqVCfSfQuw/ClKZiw/2N2QN9ysVgHm5Hu2EW4UHpGiusHRGS3BEgkhM3H/M
bbH/SAVlrlmQuXiCebygcgHOdeSxI5l0Bi7UG7uQPEH+4+oJ/kMoc/HAiaJKBYh+/uF3GWwU
lM7wIwp+UEmEANoCKjBQQThz8cBuZeUCHPqdx46E0xktsbQj6kLgP214+Q9krhX8rT/qYbRy
C7oxXOjukM4W8U1ndBZ+UFFly8n7Tw++/oOJzIfMJRTMpd6VCsBanqFjuWQ0wDfVTIq/CxVS
IvKfaZC5BOPwn6z+Tswgpr+DTpaS+WNb+KYzWkrWhfBWptY18bAUn4t3HM5cckHWDzieD+8m
Y7ajXd+Ym6PQLorAZbCOYzoDF+qpxKZB0H+c3fEFwCtzraEInP4uOXOtnHV8iPuVZNiLexI8
QhmpdBYcqNCScyFNPhUYoOCeuaRoCYmLd39j9uW6SMjNdS6IZY0PfiQDgRVI0Tzu6YyWmtsI
diHwn1ZK7v4jQbMFZS54D/P9ZSTL8B1P9xmZBzN+zcfxxjbZ997hYG4u5OpByoXkzm5KRHO0
/kmCM9du5ffBUI9W8CdKTJD9fBQd/VdoOhvLLZ0FsAsVUAT8J4/y9+foP6MFZ67Df7Dv90aQ
n8AHGvCegLncD+2U8ddgNdd0JjW3FuxCf+PZU+w/XP7uMGGZa6eUudCNNT9NwL+rCTq+T2vt
ayAonQ2RcHCh7sJdSI5nTxGd8MwFKff79IPfkrB/WcYiVn0ZnSxJTjrDjy7afEqY/yjw7Cmi
k5K5juex/7V3Dz5yhVEUwP+cce2GjWu7cW3btm03qm27QRXVtt2ZbO8op/r2vp7qS+a+uHHP
5r7z252ze2N7UUrZZxMB0FBw6GxQUJ1JdXlEXSHcn3oB7g/MFSPN5a75fyEAQGG5QIHUWe9I
wCskBYa4Qrg/rfADSNZces1Poeb/swAoKEBnM4Lq7H372B32Ct2RAUxb3B/KXHzN/wcBcFCA
zor92sQVIic01eTzprg/pLn0mn/Hgz/mKVC4moECobMgV4gd8snnTfWM5fTL/G1ZlK75HgTA
QUGu7eJAOhNG6RMaboDXKWOuhTAXUfM9CICGAnTGD/m4AR7MNQunn6j5HgTAQgEv5CnQGTHk
IwZ4MNfE+C80iE2o+Z4GgBTSUOgFKKg6G41vl5JDPmKANyKAuVDzO6HmexAAAQVSZxjy1cMV
ogd4OP0yc1uimgs1Hx9n8zIAHgp4GSwQnUWZCQ0xwBNzzYO5yJrvfwCAwmmBQklGZ8SQDwM8
t7mm4cVL1HzvA+ChEE5OcOoMc2JqgAdzjcU3O4ma70EAPBQup/a3cUEBOhse168QMcCDuSLB
aj7xu329CICHAnTWHzrThnz6AA//+30VcxE1388AeChAZz0jxJAPAzynuYia738AxPPqRgYK
sWJ1Fv7xCgmvlAHMtwM8mGsSzKXW/AIIQIUCdKYP+fQBnkzYVkQcNb8ian5hBQAoNMPX5nc6
Gwyd6UM+DPB0cyk1vwACUKAAnfWJ6kO+YgZ4vcRcePHqNb9gAlCggJfBTPyaLveQzzHA6wZz
OWu+BaBAATpThnx3McBzmctR8y0ABQrQmXvIhwGe21zrSqfOjUfNtwB0KEBnUegsN+SLOQd4
MJde8y0ARwqAQj6DudBZZsiXcA5gekSSs2EureZbAAoUquKFPDWns++HfBjgwVyo+RfmoeZb
ADQUcjobk9HZN0M+DPBgLtT8I0TNtwDcUFiW0dm3Qz7cn4E5c2Vq/gCm5lsAChSgs+wVwgAP
5krX/LV8zbcAFCisjiRnxpI9wrkhX3qAlxCsibnYD+1YAAQUJkQ/dozL8ZEBzIf28eTYaHJt
Ga7mWwAEFPalNtdNDo89bphIfwBdzLWhBlnzLQD+JwoH+7/qVvFlpwqpPT34mm8B8M/n15+P
Lf90cGHRpxf4RwvAHt8DsMcCsADssQAsAHssAAvAni8AV5380akCdgAAAABJRU5ErkJggg==
--B_3664825007_384940722--
--B_3664825007_1904734766
Content-type: application/pkcs7-signature; name="smime.p7s"
Content-transfer-encoding: base64
Content-disposition: attachment;
filename="smime.p7s"
MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s
b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G
A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1
R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ
OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/
0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m
D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2
NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940
tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl
gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5
4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM
tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px
wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA
MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ
MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg
VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw
HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO
MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
--B_3664825007_1904734766--

View file

@ -2,15 +2,15 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { dispatch } from 'codesandbox-api';
import smooshpack from 'smooshpack';
import { SandpackClient } from '@codesandbox/sandpack-client';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Clientside from '~/ide/components/preview/clientside.vue';
import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
jest.mock('smooshpack', () => ({
Manager: jest.fn(),
jest.mock('@codesandbox/sandpack-client', () => ({
SandpackClient: jest.fn(),
}));
Vue.use(Vuex);
@ -78,8 +78,8 @@ describe('IDE clientside preview', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
sandpackReady: true,
manager: {
listener: jest.fn(),
client: {
cleanup: jest.fn(),
updatePreview: jest.fn(),
},
});
@ -90,9 +90,9 @@ describe('IDE clientside preview', () => {
});
describe('without main entry', () => {
it('creates sandpack manager', () => {
it('creates sandpack client', () => {
createComponent();
expect(smooshpack.Manager).not.toHaveBeenCalled();
expect(SandpackClient).not.toHaveBeenCalled();
});
});
describe('with main entry', () => {
@ -102,8 +102,8 @@ describe('IDE clientside preview', () => {
return waitForPromises();
});
it('creates sandpack manager', () => {
expect(smooshpack.Manager).toHaveBeenCalledWith(
it('creates sandpack client', () => {
expect(SandpackClient).toHaveBeenCalledWith(
'#ide-preview',
expectedSandpackOptions(),
expectedSandpackSettings(),
@ -141,8 +141,8 @@ describe('IDE clientside preview', () => {
return waitForPromises();
});
it('creates sandpack manager with bundlerURL', () => {
expect(smooshpack.Manager).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
it('creates sandpack client with bundlerURL', () => {
expect(SandpackClient).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
...expectedSandpackSettings(),
bundlerURL: TEST_BUNDLER_URL,
});
@ -156,8 +156,8 @@ describe('IDE clientside preview', () => {
return waitForPromises();
});
it('creates sandpack manager', () => {
expect(smooshpack.Manager).toHaveBeenCalledWith(
it('creates sandpack client', () => {
expect(SandpackClient).toHaveBeenCalledWith(
'#ide-preview',
{
files: {},
@ -332,7 +332,7 @@ describe('IDE clientside preview', () => {
});
describe('update', () => {
it('initializes manager if manager is empty', () => {
it('initializes client if client is empty', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@ -340,7 +340,7 @@ describe('IDE clientside preview', () => {
wrapper.vm.update();
return waitForPromises().then(() => {
expect(smooshpack.Manager).toHaveBeenCalled();
expect(SandpackClient).toHaveBeenCalled();
});
});
@ -349,7 +349,7 @@ describe('IDE clientside preview', () => {
wrapper.vm.update();
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
});
});
@ -361,7 +361,7 @@ describe('IDE clientside preview', () => {
});
it('calls updatePreview', () => {
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
});
});
});
@ -405,7 +405,7 @@ describe('IDE clientside preview', () => {
beforeEach(() => {
createInitializedComponent();
spy = wrapper.vm.manager.updatePreview;
spy = wrapper.vm.client.updatePreview;
wrapper.destroy();
});

View file

@ -11,7 +11,7 @@ jest.mock('codesandbox-api', () => ({
describe('IDE clientside preview navigator', () => {
let wrapper;
let manager;
let client;
let listenHandler;
const findBackButton = () => wrapper.findAll('button').at(0);
@ -20,9 +20,9 @@ describe('IDE clientside preview navigator', () => {
beforeEach(() => {
listen.mockClear();
manager = { bundlerURL: TEST_HOST, iframe: { src: '' } };
client = { bundlerURL: TEST_HOST, iframe: { src: '' } };
wrapper = shallowMount(ClientsideNavigator, { propsData: { manager } });
wrapper = shallowMount(ClientsideNavigator, { propsData: { client } });
[[listenHandler]] = listen.mock.calls;
});
@ -31,7 +31,7 @@ describe('IDE clientside preview navigator', () => {
});
it('renders readonly URL bar', async () => {
listenHandler({ type: 'urlchange', url: manager.bundlerURL });
listenHandler({ type: 'urlchange', url: client.bundlerURL });
await nextTick();
expect(wrapper.find('input[readonly]').element.value).toBe('/');
});
@ -89,13 +89,13 @@ describe('IDE clientside preview navigator', () => {
expect(findBackButton().attributes('disabled')).toBe('disabled');
});
it('updates manager iframe src', async () => {
it('updates client iframe src', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
await nextTick();
findBackButton().trigger('click');
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
});
});
@ -133,13 +133,13 @@ describe('IDE clientside preview navigator', () => {
expect(findForwardButton().attributes('disabled')).toBe('disabled');
});
it('updates manager iframe src', async () => {
it('updates client iframe src', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
await nextTick();
findBackButton().trigger('click');
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
});
});
@ -152,10 +152,10 @@ describe('IDE clientside preview navigator', () => {
});
it('calls refresh with current path', () => {
manager.iframe.src = 'something-other';
client.iframe.src = 'something-other';
findRefreshButton().trigger('click');
expect(manager.iframe.src).toBe(url);
expect(client.iframe.src).toBe(url);
});
});
});

View file

@ -38,7 +38,7 @@ export default [
'</tr>\n',
'</table>',
].join(''),
output: '<table>',
output: '<table data-myattr=&quot;XSS&quot;>',
},
],
// Note: style is sanitized out
@ -98,7 +98,7 @@ export default [
'</svg>',
].join(),
output:
'<svg xmlns="http://www.w3.org/2000/svg" width="388.84pt" version="1.0" id="svg2" height="115.02pt">',
'<svg height=&quot;115.02pt&quot; id=&quot;svg2&quot; version=&quot;1.0&quot; width=&quot;388.84pt&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;>',
},
],
];

View file

@ -49,15 +49,17 @@ describe('Output component', () => {
const htmlType = json.cells[4];
createComponent(htmlType.outputs[0]);
expect(wrapper.findAll('p')).toHaveLength(1);
expect(wrapper.text()).toContain('test');
const iframe = wrapper.find('iframe');
expect(iframe.exists()).toBe(true);
expect(iframe.element.getAttribute('sandbox')).toBe('');
expect(iframe.element.getAttribute('srcdoc')).toBe('<p>test</p>');
});
it('renders multiple raw HTML outputs', () => {
const htmlType = json.cells[4];
createComponent([htmlType.outputs[0], htmlType.outputs[0]]);
expect(wrapper.findAll('p')).toHaveLength(2);
expect(wrapper.findAll('iframe')).toHaveLength(2);
});
});
@ -84,7 +86,11 @@ describe('Output component', () => {
});
it('renders as an svg', () => {
expect(wrapper.find('svg').exists()).toBe(true);
const iframe = wrapper.find('iframe');
expect(iframe.exists()).toBe(true);
expect(iframe.element.getAttribute('sandbox')).toBe('');
expect(iframe.element.getAttribute('srcdoc')).toBe('<svg></svg>');
});
});

View file

@ -320,7 +320,7 @@ RSpec.describe CommitsHelper do
let(:current_path) { "test" }
before do
expect(commit).to receive(:status_for).with(ref).and_return(commit_status)
expect(commit).to receive(:detailed_status_for).with(ref).and_return(commit_status)
assign(:path, current_path)
end

View file

@ -112,6 +112,14 @@ RSpec.describe LabelsHelper do
end
end
describe 'render_label_text' do
it 'html escapes the bg_color correctly' do
xss_payload = '"><img src=x onerror=prompt(1)>'
label_text = render_label_text('xss', bg_color: xss_payload)
expect(label_text).to include(html_escape(xss_payload))
end
end
describe 'text_color_for_bg' do
it 'uses light text on dark backgrounds' do
expect(text_color_for_bg('#222E2E')).to be_color('#FFFFFF')

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Rack VULNDB-255039' do
context 'when handling query params in GET requests' do
it 'does not treat semicolons as query delimiters' do
env = ::Rack::MockRequest.env_for('http://gitlab.com?a=b;c=1')
query_hash = ::Rack::Request.new(env).GET
# Prior to this patch, this was splitting around the semicolon, which
# would return {"a"=>"b", "c"=>"1"}
expect(query_hash).to eq({ "a" => "b;c=1" })
end
end
end

View file

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'sawyer'
require_relative '../../config/initializers/sawyer_patch'
RSpec.describe 'sawyer_patch' do
it 'raises error when acessing a method that overlaps a Ruby method' do
sawyer_resource = Sawyer::Resource.new(
Sawyer::Agent.new(''),
{
to_s: 'Overriding method',
user: { to_s: 'Overriding method', name: 'User name' }
}
)
error_message = 'Sawyer method "to_s" overlaps Ruby method. Convert to a hash to access the attribute.'
expect { sawyer_resource.to_s }.to raise_error(Sawyer::Error, error_message)
expect { sawyer_resource.to_s? }.to raise_error(Sawyer::Error, error_message)
expect { sawyer_resource.to_s = 'new value' }.to raise_error(Sawyer::Error, error_message)
expect { sawyer_resource.user.to_s }.to raise_error(Sawyer::Error, error_message)
expect(sawyer_resource.user.name).to eq('User name')
end
it 'raises error when acessing a boolean method that overlaps a Ruby method' do
sawyer_resource = Sawyer::Resource.new(
Sawyer::Agent.new(''),
{
nil?: 'value'
}
)
expect { sawyer_resource.nil? }.to raise_error(Sawyer::Error)
end
it 'raises error when acessing a method that expects an argument' do
sawyer_resource = Sawyer::Resource.new(
Sawyer::Agent.new(''),
{
'user': 'value',
'user=': 'value',
'==': 'value',
'!=': 'value',
'+': 'value'
}
)
expect(sawyer_resource.user).to eq('value')
expect { sawyer_resource.user = 'New user' }.to raise_error(ArgumentError)
expect { sawyer_resource == true }.to raise_error(ArgumentError)
expect { sawyer_resource != true }.to raise_error(ArgumentError)
expect { sawyer_resource + 1 }.to raise_error(ArgumentError)
end
it 'does not raise error if is not an overlapping method' do
sawyer_resource = Sawyer::Resource.new(
Sawyer::Agent.new(''),
{
count_total: 1,
user: { name: 'User name' }
}
)
expect(sawyer_resource.count_total).to eq(1)
expect(sawyer_resource.count_total?).to eq(true)
expect(sawyer_resource.count_total + 1).to eq(2)
expect(sawyer_resource.user.name).to eq('User name')
end
end

View file

@ -18,10 +18,20 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
context 'detects' do
let(:email) { FFaker::Internet.email }
it 'trailers in the form of *-by and replace users with links' do
doc = filter(commit_message_html)
context 'trailers in the form of *-by' do
where(:commit_trailer) do
["#{FFaker::Lorem.word}-by:", "#{FFaker::Lorem.word}-BY:", "#{FFaker::Lorem.word}-By:"]
end
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
with_them do
let(:trailer) { commit_trailer }
it 'replaces users with links' do
doc = filter(commit_message_html)
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
end
end
end
it 'trailers prefixed with whitespaces' do
@ -121,7 +131,14 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
context "ignores" do
it 'commit messages without trailers' do
exp = message = commit_html(FFaker::Lorem.sentence)
exp = message = commit_html(Array.new(5) { FFaker::Lorem.sentence }.join("\n"))
doc = filter(message)
expect(doc.to_html).to match Regexp.escape(exp)
end
it 'trailers without emails' do
exp = message = commit_html(Array.new(5) { 'Merged-By:' }.join("\n"))
doc = filter(message)
expect(doc.to_html).to match Regexp.escape(exp)

View file

@ -92,5 +92,50 @@ RSpec.describe Banzai::Filter::ImageLinkFilter do
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
end
context 'when link attributes contain malicious code' do
let(:malicious_code) do
# rubocop:disable Layout/LineLength
%q(<a class='fixed-top fixed-bottom' data-create-path=/malicious-url><style> .tab-content>.tab-pane{display: block !important}</style>)
# rubocop:enable Layout/LineLength
end
context 'when image alt contains malicious code' do
it 'ignores image alt and uses image path as the link text', :aggregate_failures do
doc = filter(image(path, alt: malicious_code), context)
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
end
context 'when image src contains malicious code' do
it 'ignores image src and does not use it as the link text' do
doc = filter(image(malicious_code), context)
expect(doc.to_html).to match(%r{^<a[^>]*></a>$})
end
it 'keeps image src unchanged, malicious code does not execute as part of url' do
doc = filter(image(malicious_code), context)
expect(doc.at_css('a')['href']).to eq(malicious_code)
end
end
context 'when image data-src contains malicious code' do
it 'ignores data-src and uses image path as the link text', :aggregate_failures do
doc = filter(image(path, data_src: malicious_code), context)
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
end
it 'uses image data-src, malicious code does not execute as part of url' do
doc = filter(image(path, data_src: malicious_code), context)
expect(doc.at_css('a')['href']).to eq(malicious_code)
end
end
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::PathologicalMarkdownFilter do
include FilterSpecHelper
let_it_be(:short_text) { '![a' * 5 }
let_it_be(:long_text) { ([short_text] * 10).join(' ') }
let_it_be(:with_images_text) { "![One ![one](one.jpg) #{'and\n' * 200} ![two ![two](two.jpg)" }
it 'detects a significat number of unclosed image links' do
msg = <<~TEXT
_Unable to render markdown - too many unclosed markdown image links detected._
TEXT
expect(filter(long_text)).to eq(msg.strip)
end
it 'does nothing when there are only a few unclosed image links' do
expect(filter(short_text)).to eq(short_text)
end
it 'does nothing when there are only a few unclosed image links and images' do
expect(filter(with_images_text)).to eq(with_images_text)
end
end

View file

@ -167,4 +167,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(output).to include('<em>@test_</em>')
end
end
describe 'unclosed image links' do
it 'detects a significat number of unclosed image links' do
markdown = '![a ' * 30
msg = <<~TEXT
Unable to render markdown - too many unclosed markdown image links detected.
TEXT
output = described_class.to_html(markdown, project: nil)
expect(output).to include(msg.strip)
end
end
end

View file

@ -9,12 +9,13 @@ RSpec.describe Gitlab::Git::Tree do
let(:repository) { project.repository.raw }
shared_examples :repo do
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, pagination_params) }
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, pagination_params) }
let(:sha) { SeedRepo::Commit::ID }
let(:path) { nil }
let(:recursive) { false }
let(:pagination_params) { nil }
let(:skip_flat_paths) { false }
let(:entries) { tree.first }
let(:cursor) { tree.second }
@ -107,6 +108,12 @@ RSpec.describe Gitlab::Git::Tree do
end
it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') }
context 'when skip_flat_paths is true' do
let(:skip_flat_paths) { true }
it { expect(subdir_file.flat_path).to be_blank }
end
end
end
@ -162,7 +169,7 @@ RSpec.describe Gitlab::Git::Tree do
allow(instance).to receive(:lookup).with(SeedRepo::Commit::ID)
end
described_class.where(repository, SeedRepo::Commit::ID, 'files', false)
described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
end
it_behaves_like :repo do
@ -180,7 +187,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:entries_count) { entries.count }
it 'returns all entries without a cursor' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: entries_count, page_token: nil })
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: entries_count, page_token: nil })
expect(cursor).to be_nil
expect(result.entries.count).to eq(entries_count)
@ -209,7 +216,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:entries_count) { entries.count }
it 'returns all entries' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: nil })
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: nil })
expect(result.count).to eq(entries_count)
expect(cursor).to be_nil
@ -220,7 +227,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:token) { entries.second.id }
it 'returns all entries after token' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: token })
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: token })
expect(result.count).to eq(entries.count - 2)
expect(cursor).to be_nil
@ -252,7 +259,7 @@ RSpec.describe Gitlab::Git::Tree do
expected_entries = entries
loop do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: 5, page_token: token })
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: 5, page_token: token })
collected_entries += result.entries
token = cursor&.next_cursor

View file

@ -150,16 +150,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
describe '#tree_entries' do
subject { client.tree_entries(repository, revision, path, recursive, pagination_params) }
subject { client.tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params) }
let(:path) { '/' }
let(:recursive) { false }
let(:pagination_params) { nil }
let(:skip_flat_paths) { false }
it 'sends a get_tree_entries message' do
it 'sends a get_tree_entries message with default limit' do
expected_pagination_params = Gitaly::PaginationParameter.new(limit: Gitlab::GitalyClient::CommitService::TREE_ENTRIES_DEFAULT_LIMIT)
expect_any_instance_of(Gitaly::CommitService::Stub)
.to receive(:get_tree_entries)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
.and_return([])
is_expected.to eq([[], nil])
@ -189,9 +191,10 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
pagination_cursor: pagination_cursor
)
expected_pagination_params = Gitaly::PaginationParameter.new(limit: 3)
expect_any_instance_of(Gitaly::CommitService::Stub)
.to receive(:get_tree_entries)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
.and_return([response])
is_expected.to eq([[], pagination_cursor])

View file

@ -72,4 +72,18 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
it { is_expected.to be(true) }
end
end
describe 'count' do
subject { cache.count(cache_prefix) }
it { is_expected.to be(0) }
context 'item added' do
before do
cache.write(cache_prefix, 'test_item')
end
it { is_expected.to be(1) }
end
end
end

View file

@ -2,17 +2,21 @@
require 'spec_helper'
RSpec.describe Gitlab::Zentao::Client do
subject(:integration) { described_class.new(zentao_integration) }
RSpec.describe Gitlab::Zentao::Client, :clean_gitlab_redis_cache do
subject(:client) { described_class.new(zentao_integration) }
let(:zentao_integration) { create(:zentao_integration) }
def mock_get_products_url
integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
client.send(:url, "products/#{zentao_integration.zentao_product_xid}")
end
def mock_fetch_issues_url
client.send(:url, "products/#{zentao_integration.zentao_product_xid}/issues")
end
def mock_fetch_issue_url(issue_id)
integration.send(:url, "issues/#{issue_id}")
client.send(:url, "issues/#{issue_id}")
end
let(:mock_headers) do
@ -29,13 +33,13 @@ RSpec.describe Gitlab::Zentao::Client do
let(:zentao_integration) { nil }
it 'raises ConfigError' do
expect { integration }.to raise_error(described_class::ConfigError)
expect { client }.to raise_error(described_class::ConfigError)
end
end
context 'integration is provided' do
it 'is initialized successfully' do
expect { integration }.not_to raise_error
expect { client }.not_to raise_error
end
end
end
@ -50,7 +54,7 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
expect(client.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
end
end
@ -62,8 +66,8 @@ RSpec.describe Gitlab::Zentao::Client do
it 'fetches the empty product' do
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error, 'request error')
client.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::RequestError)
end
end
@ -75,7 +79,7 @@ RSpec.describe Gitlab::Zentao::Client do
it 'fetches the empty product' do
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
client.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error, 'invalid response format')
end
end
@ -89,7 +93,7 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'responds with success' do
expect(integration.ping[:success]).to eq true
expect(client.ping[:success]).to eq true
end
end
@ -100,7 +104,69 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'responds with unsuccess' do
expect(integration.ping[:success]).to eq false
expect(client.ping[:success]).to eq false
end
end
end
describe '#fetch_issues' do
let(:mock_response) { { 'issues' => [{ 'id' => 'story-1' }, { 'id' => 'bug-11' }] } }
before do
WebMock.stub_request(:get, mock_fetch_issues_url)
.with(mock_headers).to_return(status: 200, body: mock_response.to_json)
end
it 'returns the response' do
expect(client.fetch_issues).to eq(mock_response)
end
describe 'marking the issues as seen in the product' do
let(:cache) { ::Gitlab::SetCache.new }
let(:cache_key) do
[
:zentao_product_issues,
OpenSSL::Digest::SHA256.hexdigest(zentao_integration.client_url),
zentao_integration.zentao_product_xid
].join(':')
end
it 'adds issue ids to the cache' do
expect { client.fetch_issues }.to change { cache.read(cache_key) }
.from(be_empty)
.to match_array(%w[bug-11 story-1])
end
it 'does not add issue ids to the cache if max set size has been reached' do
cache.write(cache_key, %w[foo bar])
stub_const("#{described_class}::CACHE_MAX_SET_SIZE", 1)
client.fetch_issues
expect(cache.read(cache_key)).to match_array(%w[foo bar])
end
it 'does not duplicate issue ids in the cache' do
client.fetch_issues
client.fetch_issues
expect(cache.read(cache_key)).to match_array(%w[bug-11 story-1])
end
it 'touches the cache ttl every time issues are fetched' do
fresh_ttl = 1.month.to_i
freeze_time do
client.fetch_issues
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
end
travel_to(1.minute.from_now) do
client.fetch_issues
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
end
end
end
end
@ -109,9 +175,9 @@ RSpec.describe Gitlab::Zentao::Client do
context 'with invalid id' do
let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
it 'returns empty object' do
it 'raises Error' do
invalid_ids.each do |id|
expect { integration.fetch_issue(id) }
expect { client.fetch_issue(id) }
.to raise_error(Gitlab::Zentao::Client::Error, 'invalid issue id')
end
end
@ -120,12 +186,31 @@ RSpec.describe Gitlab::Zentao::Client do
context 'with valid id' do
let(:valid_ids) { %w[story-1 bug-23] }
it 'fetches current issue' do
valid_ids.each do |id|
WebMock.stub_request(:get, mock_fetch_issue_url(id))
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
context 'when issue has been seen on the index' do
before do
issues_body = { issues: valid_ids.map { { id: _1 } } }.to_json
expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
WebMock.stub_request(:get, mock_fetch_issues_url)
.with(mock_headers).to_return(status: 200, body: issues_body)
client.fetch_issues
end
it 'fetches the issue' do
valid_ids.each do |id|
WebMock.stub_request(:get, mock_fetch_issue_url(id))
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
expect(client.fetch_issue(id).dig('issue', 'id')).to eq id
end
end
end
context 'when issue has not been seen on the index' do
it 'raises RequestError' do
valid_ids.each do |id|
expect { client.fetch_issue(id) }.to raise_error(Gitlab::Zentao::Client::RequestError)
end
end
end
end
@ -135,7 +220,7 @@ RSpec.describe Gitlab::Zentao::Client do
context 'api url' do
shared_examples 'joins api_url correctly' do
it 'verify url' do
expect(integration.send(:url, "products/1").to_s)
expect(client.send(:url, "products/1").to_s)
.to eq("https://jihudemo.zentao.net/zentao/api.php/v1/products/1")
end
end
@ -157,7 +242,7 @@ RSpec.describe Gitlab::Zentao::Client do
let(:zentao_integration) { create(:zentao_integration, url: 'https://jihudemo.zentao.net') }
it 'joins url correctly' do
expect(integration.send(:url, "products/1").to_s)
expect(client.send(:url, "products/1").to_s)
.to eq("https://jihudemo.zentao.net/api.php/v1/products/1")
end
end

View file

@ -81,4 +81,24 @@ RSpec.describe Integrations::Zentao do
expect(zentao_integration.help).not_to be_empty
end
end
describe '#client_url' do
subject(:integration) { build(:zentao_integration, api_url: api_url, url: 'url').client_url }
context 'when api_url is set' do
let(:api_url) { 'api_url' }
it 'returns the api_url' do
is_expected.to eq(api_url)
end
end
context 'when api_url is not set' do
let(:api_url) { '' }
it 'returns the url' do
is_expected.to eq('url')
end
end
end
end

View file

@ -823,13 +823,21 @@ RSpec.describe Issue do
end
describe '#to_branch_name exists ending with -index' do
before do
it 'returns #to_branch_name ending with max index + 1' do
allow(repository).to receive(:branch_exists?).and_return(true)
allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
end
it 'returns #to_branch_name ending with max index + 1' do
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
context 'when branch name still exists after 5 attempts' do
it 'returns #to_branch_name ending with random characters' do
allow(repository).to receive(:branch_exists?).with(subject.to_branch_name).and_return(true)
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(true)
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\h{8}/).and_return(false)
expect(subject.suggested_branch_name).to match(/#{subject.to_branch_name}-\h{8}/)
end
end
end
end

View file

@ -2625,7 +2625,7 @@ RSpec.describe Repository do
end
shared_examples '#tree' do
subject { repository.tree(sha, path, recursive: recursive, pagination_params: pagination_params) }
subject { repository.tree(sha, path, recursive: recursive, skip_flat_paths: false, pagination_params: pagination_params) }
let(:sha) { :head }
let(:path) { nil }

View file

@ -91,6 +91,45 @@ RSpec.describe Snippet do
end
end
end
context 'description validations' do
let_it_be(:invalid_description) { 'a' * (described_class::DESCRIPTION_LENGTH_MAX * 2) }
context 'with existing snippets' do
let(:snippet) { create(:personal_snippet, description: 'This is a valid content at the time of creation') }
it 'does not raise a validation error if the description is not changed' do
snippet.title = 'new title'
expect(snippet).to be_valid
end
it 'raises and error if the description is changed and the size is bigger than limit' do
expect(snippet).to be_valid
snippet.description = invalid_description
expect(snippet).not_to be_valid
end
end
context 'with new snippets' do
it 'is valid when description is smaller than the limit' do
snippet = build(:personal_snippet, description: 'Valid Desc')
expect(snippet).to be_valid
end
it 'raises error when description is bigger than setting limit' do
snippet = build(:personal_snippet, description: invalid_description)
aggregate_failures do
expect(snippet).not_to be_valid
expect(snippet.errors.messages_for(:description)).to include("is too long (2 MB). The maximum size is 1 MB.")
end
end
end
end
end
describe 'callbacks' do

View file

@ -12,29 +12,51 @@ RSpec.describe CommitPresenter do
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/commit/#{commit.sha}") }
end
describe '#status_for' do
subject { presenter.status_for('ref') }
describe '#detailed_status_for' do
using RSpec::Parameterized::TableSyntax
context 'when user can read_commit_status' do
before do
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(true)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha, ref: 'ref') }
it 'returns commit status for ref' do
pipeline = double
status = double
subject { presenter.detailed_status_for('ref')&.text }
expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline)
expect(pipeline).to receive(:detailed_status).with(user).and_return(status)
expect(subject).to eq(status)
end
where(:read_commit_status, :read_pipeline, :expected_result) do
true | true | 'passed'
true | false | nil
false | true | nil
false | false | nil
end
context 'when user can not read_commit_status' do
it 'is nil' do
is_expected.to eq(nil)
with_them do
before do
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
end
it { is_expected.to eq expected_result }
end
end
describe '#status_for' do
using RSpec::Parameterized::TableSyntax
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha) }
subject { presenter.status_for }
where(:read_commit_status, :read_pipeline, :expected_result) do
true | true | 'success'
true | false | nil
false | true | nil
false | false | nil
end
with_them do
before do
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
end
it { is_expected.to eq expected_result }
end
end

View file

@ -6,11 +6,16 @@ RSpec.describe 'getting incident timeline events' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:issue) { create(:issue, project: private_project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:updated_by_user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:another_incident) { create(:incident, project: project) }
let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) }
let_it_be(:issue_url) { project_issue_url(private_project, issue) }
let_it_be(:issue_ref) { "#{private_project.full_path}##{issue.iid}" }
let_it_be(:issue_link) { %Q(<a href="#{issue_url}">#{issue_url}</a>) }
let_it_be(:timeline_event) do
create(
@ -18,7 +23,8 @@ RSpec.describe 'getting incident timeline events' do
incident: incident,
project: project,
updated_by_user: updated_by_user,
promoted_from_note: promoted_from_note
promoted_from_note: promoted_from_note,
note: "Referencing #{issue.to_reference(full: true)} - Full URL #{issue_url}"
)
end
@ -89,7 +95,7 @@ RSpec.describe 'getting incident timeline events' do
'title' => incident.title
},
'note' => timeline_event.note,
'noteHtml' => timeline_event.note_html,
'noteHtml' => "<p>Referencing #{issue_ref} - Full URL #{issue_link}</p>",
'promotedFromNote' => {
'id' => promoted_from_note.to_global_id.to_s,
'body' => promoted_from_note.note

View file

@ -763,6 +763,96 @@ RSpec.describe API::Search do
it_behaves_like 'pagination', scope: :commits, search: 'merge'
it_behaves_like 'ping counters', scope: :commits
describe 'pipeline visibility' do
shared_examples 'pipeline information visible' do
it 'contains status and last_pipeline' do
request
expect(json_response[0]['status']).to eq 'success'
expect(json_response[0]['last_pipeline']).not_to be_nil
end
end
shared_examples 'pipeline information not visible' do
it 'does not contain status and last_pipeline' do
request
expect(json_response[0]['status']).to be_nil
expect(json_response[0]['last_pipeline']).to be_nil
end
end
let(:request) { get api(endpoint, user), params: { scope: 'commits', search: repo_project.commit.sha } }
before do
create(:ci_pipeline, :success, project: repo_project, sha: repo_project.commit.sha)
end
context 'with non public pipeline' do
let_it_be(:repo_project) do
create(:project, :public, :repository, public_builds: false, group: group)
end
context 'user is project member with reporter role or above' do
before do
repo_project.add_reporter(user)
end
it_behaves_like 'pipeline information visible'
end
context 'user is project member with guest role' do
before do
repo_project.add_guest(user)
end
it_behaves_like 'pipeline information not visible'
end
context 'user is not project member' do
let_it_be(:user) { create(:user) }
it_behaves_like 'pipeline information not visible'
end
end
context 'with public pipeline' do
let_it_be(:repo_project) do
create(:project, :public, :repository, public_builds: true, group: group)
end
context 'user is project member with reporter role or above' do
before do
repo_project.add_reporter(user)
end
it_behaves_like 'pipeline information visible'
end
context 'user is project member with guest role' do
before do
repo_project.add_guest(user)
end
it_behaves_like 'pipeline information visible'
end
context 'user is not project member' do
let_it_be(:user) { create(:user) }
it_behaves_like 'pipeline information visible'
context 'when CI/CD is set to only project members' do
before do
repo_project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'pipeline information not visible'
end
end
end
end
end
context 'for commits scope with project path as id' do

View file

@ -643,17 +643,17 @@ RSpec.describe 'Git HTTP requests' do
end
context 'when username and password are provided' do
it 'rejects pulls with personal access token error message' do
it 'rejects pulls with generic error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
it 'rejects the push attempt with personal access token error message' do
it 'rejects the push attempt with generic error message' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
end
@ -750,17 +750,17 @@ RSpec.describe 'Git HTTP requests' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
end
it 'rejects pulls with personal access token error message' do
it 'rejects pulls with generic error message' do
download(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
it 'rejects pushes with personal access token error message' do
it 'rejects pushes with generic error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
@ -771,10 +771,10 @@ RSpec.describe 'Git HTTP requests' do
.to receive(:login).and_return(nil)
end
it 'does not display the personal access token error message' do
it 'displays the generic error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
end
@ -1300,17 +1300,18 @@ RSpec.describe 'Git HTTP requests' do
end
context 'when username and password are provided' do
it 'rejects pulls with personal access token error message' do
it 'rejects pulls with generic error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
it 'rejects the push attempt with personal access token error message' do
it 'rejects the push attempt with generic error message' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
end
@ -1381,17 +1382,17 @@ RSpec.describe 'Git HTTP requests' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
end
it 'rejects pulls with personal access token error message' do
it 'rejects pulls with generic error message' do
download(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
it 'rejects pushes with personal access token error message' do
it 'rejects pushes with generic error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
@ -1402,10 +1403,10 @@ RSpec.describe 'Git HTTP requests' do
.to receive(:login).and_return(nil)
end
it 'does not display the personal access token error message' do
it 'returns a generic error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
end
end
end

View file

@ -33,6 +33,22 @@ RSpec.describe JwtController do
end
end
shared_examples "with invalid credentials" do
it "returns a generic error message" do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response).to eq(
{
"errors" => [{
"code" => "UNAUTHORIZED",
"message" => "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/user/profile/account/two_factor_authentication#troubleshooting"
}]
}
)
end
end
context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@ -51,10 +67,7 @@ RSpec.describe JwtController do
context 'with blocked user' do
let(:user) { create(:user, :blocked) }
it 'rejects the request as unauthorized' do
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('HTTP Basic: Access denied')
end
it_behaves_like 'with invalid credentials'
end
end
@ -154,10 +167,7 @@ RSpec.describe JwtController do
let(:user) { create(:user, :two_factor) }
context 'without personal token' do
it 'rejects the authorization attempt' do
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
it_behaves_like 'with invalid credentials'
end
context 'with personal token' do
@ -181,14 +191,10 @@ RSpec.describe JwtController do
context 'using invalid login' do
let(:headers) { { authorization: credentials('invalid', 'password') } }
let(:subject) { get '/jwt/auth', params: parameters, headers: headers }
context 'when internal auth is enabled' do
it 'rejects the authorization attempt' do
get '/jwt/auth', params: parameters, headers: headers
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
it_behaves_like 'with invalid credentials'
end
context 'when internal auth is disabled' do
@ -196,12 +202,7 @@ RSpec.describe JwtController do
stub_application_setting(password_authentication_enabled_for_git: false)
end
it 'rejects the authorization attempt with personal access token message' do
get '/jwt/auth', params: parameters, headers: headers
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
it_behaves_like 'with invalid credentials'
end
end
end

View file

@ -52,6 +52,8 @@ module LoginHelpers
visit new_admin_session_path
fill_in 'user_password', with: user.password
click_button 'Enter Admin Mode'
wait_for_requests
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)

View file

@ -170,6 +170,17 @@ RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member
end
end
RSpec.shared_examples 'rejected package download' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
end
end
RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
@ -330,25 +341,25 @@ RSpec.shared_examples 'pypi file download endpoint' do
using RSpec::Parameterized::TableSyntax
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token) do
:public | :developer | true | true
:public | :guest | true | true
:public | :developer | true | false
:public | :guest | true | false
:public | :developer | false | true
:public | :guest | false | true
:public | :developer | false | false
:public | :guest | false | false
:public | :anonymous | false | true
:private | :developer | true | true
:private | :guest | true | true
:private | :developer | true | false
:private | :guest | true | false
:private | :developer | false | true
:private | :guest | false | true
:private | :developer | false | false
:private | :guest | false | false
:private | :anonymous | false | true
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
:public | :developer | true | true | 'PyPI package download' | :success
:public | :guest | true | true | 'PyPI package download' | :success
:public | :developer | true | false | 'PyPI package download' | :success
:public | :guest | true | false | 'PyPI package download' | :success
:public | :developer | false | true | 'PyPI package download' | :success
:public | :guest | false | true | 'PyPI package download' | :success
:public | :developer | false | false | 'PyPI package download' | :success
:public | :guest | false | false | 'PyPI package download' | :success
:public | :anonymous | false | true | 'PyPI package download' | :success
:private | :developer | true | true | 'PyPI package download' | :success
:private | :guest | true | true | 'rejected package download' | :forbidden
:private | :developer | true | false | 'rejected package download' | :unauthorized
:private | :guest | true | false | 'rejected package download' | :unauthorized
:private | :developer | false | true | 'rejected package download' | :not_found
:private | :guest | false | true | 'rejected package download' | :not_found
:private | :developer | false | false | 'rejected package download' | :unauthorized
:private | :guest | false | false | 'rejected package download' | :unauthorized
:private | :anonymous | false | true | 'rejected package download' | :unauthorized
end
with_them do
@ -360,7 +371,7 @@ RSpec.shared_examples 'pypi file download endpoint' do
group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
end
it_behaves_like 'PyPI package download', params[:user_role], :success, params[:member]
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BytesizeValidator do
let(:model) do
Class.new do
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :content
alias_method :content_before_type_cast, :content
validates :content, bytesize: { maximum: -> { 7 } }
end.new
end
using RSpec::Parameterized::TableSyntax
where(:content, :validity, :errors) do
'short' | true | {}
'very long' | false | { content: ['is too long (9 Bytes). The maximum size is 7 Bytes.'] }
'short😁' | false | { content: ['is too long (9 Bytes). The maximum size is 7 Bytes.'] }
'short⇏' | false | { content: ['is too long (8 Bytes). The maximum size is 7 Bytes.'] }
end
with_them do
before do
model.content = content
model.validate
end
it { expect(model.valid?).to eq(validity) }
it { expect(model.errors.messages).to eq(errors) }
end
end

View file

@ -47,13 +47,12 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
context 'with ci status' do
let(:ref) { 'master' }
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
allow(view).to receive(:current_user).and_return(user)
project.add_developer(user)
create(
:ci_empty_pipeline,
ref: 'master',
@ -80,18 +79,32 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
end
context 'when pipelines are enabled' do
before do
allow(project).to receive(:builds_enabled?).and_return(true)
context 'when user has access' do
before do
project.add_developer(user)
end
it 'displays a ci status icon' do
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit
}
expect(rendered).to have_css('.ci-status-link')
end
end
it 'does display a ci status icon when pipelines are enabled' do
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit
}
context 'when user does not have access' do
it 'does not display a ci status icon' do
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit
}
expect(rendered).to have_css('.ci-status-link')
expect(rendered).not_to have_css('.ci-status-link')
end
end
end
end

View file

@ -979,6 +979,14 @@
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f"
integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==
"@codesandbox/sandpack-client@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-client/-/sandpack-client-1.2.2.tgz#e0b79c52dcbc0b622f93527dc9ff3b163467e14a"
integrity sha512-sTPQVS7mzpEm2ttpHFFSqkGd1A1tBZn7UTZwIjBNCXKHywrt9o7MyrdhUuS03J7MyXN+HSJ55Vz+OGD1Wv4ejQ==
dependencies:
codesandbox-import-utils "^1.2.3"
lodash.isequal "^4.5.0"
"@csstools/selector-specificity@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87"
@ -2936,9 +2944,9 @@ binary-extensions@^2.0.0:
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
binaryextensions@2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA==
version "2.3.0"
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
bluebird@^3.1.1, bluebird@^3.5.5:
version "3.5.5"
@ -3456,18 +3464,18 @@ codesandbox-api@0.0.23:
resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.23.tgz#bf650a21b5f3c2369e03f0c19d10b4e2ba255b4f"
integrity sha512-fFGBkIghDkQILh7iHYlpZU5sfWncCDb92FQSFE4rR3VBcTfUsD5VZgpQi+JjZQuwWIdfl4cOhcIFrUYwshUezA==
codesandbox-import-util-types@^1.2.11:
version "1.2.11"
resolved "https://registry.yarnpkg.com/codesandbox-import-util-types/-/codesandbox-import-util-types-1.2.11.tgz#68e812f21d6b309e9a52eec5cf027c3e63b4c703"
integrity sha512-n1PC/OQ0tcD9o6N5TStBB/A7tKOggUjuhnNxUU5GnVol8vmKMMLvmC6tK+8iDovQb2X2+xoDCBnl5BBgZ5OcIQ==
codesandbox-import-util-types@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/codesandbox-import-util-types/-/codesandbox-import-util-types-1.3.7.tgz#7a6097e248a75424d13b06b74368cd76bd2b3e10"
integrity sha512-8oP3emA0jyEuVOM2FBTpo/AF4C9vxHn14saVWZf2CQ/QhMtonBlNPE98ElrHkW+PFNXiO7Ad52Qr73b03n8qlA==
codesandbox-import-utils@^1.2.3:
version "1.2.11"
resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-1.2.11.tgz#b88423a4a7c785175c784c84e87f5950820280e1"
integrity sha512-KPuf7tR/SMPSRfqjWbTrYvIaW6Yt9Ajt/1FB64RsOv4BLjBNo6CwLCCPoRHYcrAKSafpWkghTZ2Bffyz7EX7AA==
version "1.3.8"
resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-1.3.8.tgz#5576786439c5f37ebd3fee5751e06027a1edef84"
integrity sha512-S12zO49QEkldoYLGh5KbkHRLOacg5BCNTue2vlyZXSpuK3oQdArwC/G1hCLKryV460bW3Ecn5xdkpfkUcFeOwQ==
dependencies:
codesandbox-import-util-types "^1.2.11"
istextorbinary "^2.2.1"
codesandbox-import-util-types "^1.3.7"
istextorbinary "2.2.1"
lz-string "^1.4.4"
collect-v8-coverage@^1.0.0:
@ -6980,7 +6988,7 @@ istanbul-reports@^3.0.0, istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
istextorbinary@^2.2.1:
istextorbinary@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
integrity sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==
@ -7935,7 +7943,7 @@ lru-cache@^6.0.0:
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
make-dir@^2.0.0:
version "2.1.0"
@ -10720,15 +10728,6 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
smooshpack@^0.0.62:
version "0.0.62"
resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.62.tgz#cb31b9f808f73de3146b050f84d044eb353b5503"
integrity sha512-lFuJV2f504/U78sifWy0V2FyoE/8mTgOXM4DL918ncNxAxbtu236XSCLAH3SQwXZWn0JdmRnWs/XU4+sIUVVmQ==
dependencies:
codesandbox-api "0.0.23"
codesandbox-import-utils "^1.2.3"
lodash.isequal "^4.5.0"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -11300,9 +11299,9 @@ text-table@^0.2.0:
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
textextensions@2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
integrity sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==
version "2.6.0"
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4"
integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==
three-orbit-controls@^82.1.0:
version "82.1.0"