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

@ -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

@ -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"