diff options
44 files changed, 670 insertions, 239 deletions
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 23ff86ba289..b89b8559921 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/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/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 8eebf9fbf6b..9e0ca28a5ea 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -36,31 +36,26 @@ class JwtController < ApplicationController @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) if @authentication_result.failed? - render_unauthorized + 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 - 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 24520a187e3..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/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 3c3179f6fbe..2e90842d09f 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -166,7 +166,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/models/snippet.rb b/app/models/snippet.rb index 2001577c88b..d7eb1860831 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -21,6 +21,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 @@ -56,19 +58,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/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 4442f62b221..3232580b999 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/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md index 13962ad0376..1fe2dcc4d92 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 af54d928bec..006dd618ce5 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:19.03.12` 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 eee6d55a3ce..17008016acc 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -336,6 +336,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 2dbeaae2267..26c86010f24 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -431,6 +431,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 dedda82091f..71594025688 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -142,7 +142,7 @@ module API Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user) 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 @@ -161,7 +161,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 5bf3c3b8aac..b6ed4103d2b 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 2e21f591667..023c993d1d6 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -183,7 +183,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 fd4d46cf77d..d8789d8839e 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -102,7 +102,7 @@ module API get do verify_search_scope!(resource: nil) - present search, with: entity + present search, with: entity, current_user: current_user end end @@ -124,7 +124,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 @@ -145,7 +145,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/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/locale/gitlab.pot b/locale/gitlab.pot index 566ec3633fc..ab945f06438 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18624,7 +18624,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" @@ -45398,6 +45398,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 f1589d5d37f..35ff46a45e1 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,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": "2.21.0", @@ -166,7 +167,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 426fbd5c04c..8e8cf4ab929 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 a199f4704f7..bccfb75835f 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/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 961e7688202..ab183a3ed19 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -312,7 +312,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/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/models/snippet_spec.rb b/spec/models/snippet_spec.rb index a54edc8510e..da94441d621 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/search_spec.rb b/spec/requests/api/search_spec.rb index 4d2a69cd85b..7d8213c48f7 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -752,6 +752,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 05b16119a0e..21d92adebce 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 70097234762..843d8f22d75 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -22,6 +22,37 @@ RSpec.describe JwtController do end end + shared_examples 'a token that expires today' do + let(:pat) { create(:personal_access_token, user: user, scopes: ['api'], expires_at: Date.today ) } + let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + + it 'fails authentication' do + expect(::Gitlab::AuthLogger).to receive(:warn).with( + hash_including(message: 'JWT authentication failed', + http_user: 'personal_access_token')).and_call_original + + get '/jwt/auth', params: parameters, headers: headers + + expect(response).to have_gitlab_http_status(:unauthorized) + 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 } @@ -40,10 +71,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 @@ -142,10 +170,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 @@ -169,14 +194,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 @@ -184,12 +205,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/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 795545e4ad1..a97b89ed3a4 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 @@ -159,6 +159,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 @@ -319,25 +330,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 @@ -349,7 +360,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 da93871e0e4..8d7bee8f7f9 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 7bbe41b71e7..95f728433cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -976,6 +976,14 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@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" + "@discoveryjs/json-ext@^0.5.0": version "0.5.6" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" @@ -3071,9 +3079,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, bluebird@~3.5.0: version "3.5.5" @@ -3662,18 +3670,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: @@ -7389,7 +7397,7 @@ istanbul-reports@^3.0.0, istanbul-reports@^3.0.2: 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== @@ -8434,7 +8442,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" @@ -11437,15 +11445,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" @@ -12062,9 +12061,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" |