diff options
author | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2022-08-30 14:44:39 +0000 |
---|---|---|
committer | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2022-08-30 14:44:39 +0000 |
commit | c8fd9c521b89b98797cfd6e3a51cae6955129c20 (patch) | |
tree | 100aa740075c645ab256c399981d84c2be394187 | |
parent | 6e8c2290dab8ae1612dff80e312911bc1147edaa (diff) | |
parent | e5a7085d4340afe6badbf6d5a808ad409cba35f2 (diff) | |
download | gitlab-ce-c8fd9c521b89b98797cfd6e3a51cae6955129c20.tar.gz |
Merge remote-tracking branch 'dev/15-3-stable' into 15-3-stable
81 files changed, 1199 insertions, 338 deletions
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index ef87efb666a..b79402ce5bf 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -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' diff --git a/CHANGELOG.md b/CHANGELOG.md index b129961cd7c..a9b2d119645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 2471c64e3c2..7bb26bde92e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -15.3.1
\ No newline at end of file +15.3.2
\ No newline at end of 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' diff --git a/Gemfile.lock b/Gemfile.lock index f04445e1d57..218a66d8f74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -1 +1 @@ -15.3.1
\ No newline at end of file +15.3.2
\ No newline at end of file diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index b1f6f2c87b9..70b881b6ff6 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -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 diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue index 96f9a85c23f..852de16d508 100644 --- a/app/assets/javascripts/ide/components/preview/navigator.vue +++ b/app/assets/javascripts/ide/components/preview/navigator.vue @@ -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}`; }, }, }; diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 2d1d8845e41..fdcea300388 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -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> diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 8eebf9fbf6b..84f5632854b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -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 diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index 8d7ba3e38c0..fbf5d82a45b 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -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) diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb index 1b4211366e0..c7e9e522c25 100644 --- a/app/graphql/resolvers/paginated_tree_resolver.rb +++ b/app/graphql/resolvers/paginated_tree_resolver.rb @@ -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) diff --git a/app/graphql/types/incident_management/timeline_event_type.rb b/app/graphql/types/incident_management/timeline_event_type.rb index a6d3f57404b..690facc8732 100644 --- a/app/graphql/types/incident_management/timeline_event_type.rb +++ b/app/graphql/types/incident_management/timeline_event_type.rb @@ -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 diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 1920650bc93..4493bc2bc6d 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -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 diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 2d0bc1bc63f..e865db128c1 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -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 diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb index 53194089296..459756c865b 100644 --- a/app/models/integrations/zentao.rb +++ b/app/models/integrations/zentao.rb @@ -69,6 +69,10 @@ module Integrations } end + def client_url + api_url.presence || url + end + def self.to_param name.demodulize.downcase end diff --git a/app/models/issue.rb b/app/models/issue.rb index 4114467eb25..df8ee34b3c3 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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 diff --git a/app/models/repository.rb b/app/models/repository.rb index eb8e45877f3..26c3b01a46e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -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) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index fd882633a44..943d09d983b 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -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 diff --git a/app/models/tree.rb b/app/models/tree.rb index fd416ebdedc..941d0394b94 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -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 diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb index 7df45ca03bb..2cb88179845 100644 --- a/app/presenters/commit_presenter.rb +++ b/app/presenters/commit_presenter.rb @@ -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) diff --git a/app/validators/bytesize_validator.rb b/app/validators/bytesize_validator.rb new file mode 100644 index 00000000000..adbdd81d5c4 --- /dev/null +++ b/app/validators/bytesize_validator.rb @@ -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 diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 71485e203db..6f44c130603 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -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) diff --git a/config/initializers/rack_VULNDB-255039_patch.rb b/config/initializers/rack_VULNDB-255039_patch.rb new file mode 100644 index 00000000000..b613ed9bdb1 --- /dev/null +++ b/config/initializers/rack_VULNDB-255039_patch.rb @@ -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 diff --git a/config/initializers/sawyer_patch.rb b/config/initializers/sawyer_patch.rb new file mode 100644 index 00000000000..08d249645cc --- /dev/null +++ b/config/initializers/sawyer_patch.rb @@ -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) diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md index 36c26a02064..484f3a100bf 100644 --- a/doc/topics/git/troubleshooting_git.md +++ b/doc/topics/git/troubleshooting_git.md @@ -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). diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index ea9435de12a..b570bba73e5 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -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 diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md index b8996dc2963..ba9ecbe50a3 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -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: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 3af033c7130..02567958356 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -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: diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 7a6c3e4d53f..50d0687ba75 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -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 diff --git a/lib/api/entities/commit.rb b/lib/api/entities/commit.rb index fd23c23b980..6cd180cd584 100644 --- a/lib/api/entities/commit.rb +++ b/lib/api/entities/commit.rb @@ -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 diff --git a/lib/api/entities/commit_detail.rb b/lib/api/entities/commit_detail.rb index 61238102e9d..cc529639359 100644 --- a/lib/api/entities/commit_detail.rb +++ b/lib/api/entities/commit_detail.rb @@ -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| diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb index 6c381d85cd8..ebedb3b7563 100644 --- a/lib/api/helpers/packages/basic_auth_helpers.rb +++ b/lib/api/helpers/packages/basic_auth_helpers.rb @@ -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! } diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index ae53f08fb1d..f8a7a3c0ecc 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -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 diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index cef72d898e6..c6a2d582d8a 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -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 diff --git a/lib/api/search.rb b/lib/api/search.rb index c78aff705ab..7aa3cf8a5cb 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -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 diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb index 5c71a18c6d0..2b51ab91c40 100644 --- a/lib/api/submodules.rb +++ b/lib/api/submodules.rb @@ -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 diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb index a615abc1989..817bea42757 100644 --- a/lib/banzai/filter/commit_trailers_filter.rb +++ b/lib/banzai/filter/commit_trailers_filter.rb @@ -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 = /<(?<author_email>[^@\s]+@[^@\s]+)>/.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('<').delete_suffix('>') + 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, diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index 60881b5f511..262c0b5340d 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -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 diff --git a/lib/banzai/filter/pathological_markdown_filter.rb b/lib/banzai/filter/pathological_markdown_filter.rb new file mode 100644 index 00000000000..0f94150c7a1 --- /dev/null +++ b/lib/banzai/filter/pathological_markdown_filter.rb @@ -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 diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb index 1da0f72996b..fb6f6e9077d 100644 --- a/lib/banzai/pipeline/plain_markdown_pipeline.rb +++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb @@ -5,6 +5,7 @@ module Banzai class PlainMarkdownPipeline < BasePipeline def self.filters FilterArray[ + Filter::PathologicalMarkdownFilter, Filter::MarkdownPreEscapeFilter, Filter::MarkdownFilter, Filter::MarkdownPostEscapeFilter diff --git a/lib/gitlab/git/rugged_impl/tree.rb b/lib/gitlab/git/rugged_impl/tree.rb index bc0af12d7e3..66cfc02130b 100644 --- a/lib/gitlab/git/rugged_impl/tree.rb +++ b/lib/gitlab/git/rugged_impl/tree.rb @@ -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 diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index eb008507397..f0eef619e13 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -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 diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 9fb34f74c82..0f306a9825d 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -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 diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb index 09ba95666de..f426f70800c 100644 --- a/lib/gitlab/markdown_cache.rb +++ b/lib/gitlab/markdown_cache.rb @@ -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) diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb index feb2c3c1d7d..896e7e3f65e 100644 --- a/lib/gitlab/set_cache.rb +++ b/lib/gitlab/set_cache.rb @@ -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) diff --git a/lib/gitlab/zentao/client.rb b/lib/gitlab/zentao/client.rb index 0c2b3049670..a9e89b99a27 100644 --- a/lib/gitlab/zentao/client.rb +++ b/lib/gitlab/zentao/client.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b86d02d5627..e7b5e6ec73c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/package.json b/package.json index ecd30dd2560..083b8b83fdf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb b/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb index 3ea71152801..87c01fb5477 100644 --- a/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb +++ b/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb @@ -16,4 +16,4 @@ install: script: - "pip install <%= package.name %> --no-deps --index-url <%= uri.scheme %>://<%= personal_access_token %>:<%= personal_access_token %>@<%= gitlab_host_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host <%= gitlab_host_with_port %>" tags: - - runner-for-<%= project.name %>
\ No newline at end of file + - runner-for-<%= project.name %> diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb index 4614eced300..22d76d684e5 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb @@ -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 diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js index cf768114e70..51e6a9d9034 100644 --- a/spec/frontend/ide/components/preview/clientside_spec.js +++ b/spec/frontend/ide/components/preview/clientside_spec.js @@ -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(); }); diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js index 9c4f825ccf5..532cb6e795c 100644 --- a/spec/frontend/ide/components/preview/navigator_spec.js +++ b/spec/frontend/ide/components/preview/navigator_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js index 70c7f56b62f..296d01ddd99 100644 --- a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js +++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js @@ -38,7 +38,7 @@ export default [ '</tr>\n', '</table>', ].join(''), - output: '<table>', + output: '<table data-myattr="XSS">', }, ], // 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="115.02pt" id="svg2" version="1.0" width="388.84pt" xmlns="http://www.w3.org/2000/svg">', }, ], ]; diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js index 4d1d03e5e34..97a7e22be60 100644 --- a/spec/frontend/notebook/cells/output/index_spec.js +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -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>'); }); }); diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index b27954de0d4..010100769d4 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -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 diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 5efa88a2a7d..90366d7772c 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -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') diff --git a/spec/initializers/rack_VULNDB-255039_patch_spec.rb b/spec/initializers/rack_VULNDB-255039_patch_spec.rb new file mode 100644 index 00000000000..754ff2f10e0 --- /dev/null +++ b/spec/initializers/rack_VULNDB-255039_patch_spec.rb @@ -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 diff --git a/spec/initializers/sawyer_patch_spec.rb b/spec/initializers/sawyer_patch_spec.rb new file mode 100644 index 00000000000..dc922654d7d --- /dev/null +++ b/spec/initializers/sawyer_patch_spec.rb @@ -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 diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb index 38f9bda57e6..c22517621c1 100644 --- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -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) diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index 6326d894b08..78d68697ac7 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -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 diff --git a/spec/lib/banzai/filter/pathological_markdown_filter_spec.rb b/spec/lib/banzai/filter/pathological_markdown_filter_spec.rb new file mode 100644 index 00000000000..e0a07d1ea77 --- /dev/null +++ b/spec/lib/banzai/filter/pathological_markdown_filter_spec.rb @@ -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 diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index 376edfb99fc..c07f99dc9fc 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -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 diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index b520de03929..2e4520cd3a0 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -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 diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 0d591fe6c43..ed6a87cda6f 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -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]) diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb index f405b2ad86e..207ac1c0eaa 100644 --- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb +++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb @@ -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 diff --git a/spec/lib/gitlab/zentao/client_spec.rb b/spec/lib/gitlab/zentao/client_spec.rb index 135f13e6265..b17ad867f0d 100644 --- a/spec/lib/gitlab/zentao/client_spec.rb +++ b/spec/lib/gitlab/zentao/client_spec.rb @@ -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 + + 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 - expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id + 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 diff --git a/spec/models/integrations/zentao_spec.rb b/spec/models/integrations/zentao_spec.rb index 4ef977ba3d2..1a32453819d 100644 --- a/spec/models/integrations/zentao_spec.rb +++ b/spec/models/integrations/zentao_spec.rb @@ -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 diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 15fe6d7625a..af4c48775ec 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -823,14 +823,22 @@ 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) - end - it 'returns #to_branch_name ending with max index + 1' do expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3") end + + 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 diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 530b03714b4..47532ed1216 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -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 } diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 38bd189f6f4..da1f2653676 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -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 diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb index b221c9ca8f7..df3ee69621b 100644 --- a/spec/presenters/commit_presenter_spec.rb +++ b/spec/presenters/commit_presenter_spec.rb @@ -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 + + let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha, ref: 'ref') } - context 'when user can read_commit_status' do + subject { presenter.detailed_status_for('ref')&.text } + + where(:read_commit_status, :read_pipeline, :expected_result) do + true | true | 'passed' + 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(true) + 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 'returns commit status for ref' do - pipeline = double - status = double + it { is_expected.to eq expected_result } + end + end - expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline) - expect(pipeline).to receive(:detailed_status).with(user).and_return(status) + describe '#status_for' do + using RSpec::Parameterized::TableSyntax - expect(subject).to eq(status) - end + 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 - 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 diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index 31fef75f679..bcbb1f11d43 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -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 diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 66b78829e0d..6034d26f1d2 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -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 diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 3ffca7e3c62..77107d0b43c 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -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 diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index db3be617a53..c9904ffa37b 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -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 diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index f83f5c7bfde..d966fd13dca 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -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) diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 1a248bb04e7..ba8311bf0be 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -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 diff --git a/spec/validators/bytesize_validator_spec.rb b/spec/validators/bytesize_validator_spec.rb new file mode 100644 index 00000000000..1914ccedd87 --- /dev/null +++ b/spec/validators/bytesize_validator_spec.rb @@ -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 diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb index 2ca23d4cb2d..4e8a680b6de 100644 --- a/spec/views/projects/commits/_commit.html.haml_spec.rb +++ b/spec/views/projects/commits/_commit.html.haml_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index c143f007aec..1195c22adb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |