Update upstream source from tag 'upstream/13.1.2'

Update to upstream version '13.1.2'
with Debian dir 9d8afabfc8
This commit is contained in:
Pirate Praveen 2020-07-02 01:49:44 +05:30
commit f2ef523b6c
49 changed files with 634 additions and 7036 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,30 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.1.2 (2020-07-01)
### Security (18 changes)
- Update xterm js dependency to latest stable 3.x version.
- Do not show activity for users with private profiles.
- Fix stored XSS in markdown renderer.
- Upgrade swagger-ui to solve XSS issues.
- Fix group deploy token API authorizations.
- Check access when sending TODOs related to merge requests.
- Change from hybrid to JSON cookies serializer.
- Prevent XSS in group name validations.
- Disable caching for wiki attachments.
- Disable Github Importer API by settings.
- Fix null byte error in upload path.
- Update permissions for time tracking endpoints.
- Add snippet repository validation after bundle import.
- Update Kaminari gem.
- Fix note author name rendering.
- Sanitize bitbucket repo urls to mitigate XSS.
- Stored XSS on the Error Tracking page.
- Fix security issue when rendering issuable.
## 13.1.1 (2020-06-23) ## 13.1.1 (2020-06-23)
### Fixed (4 changes) ### Fixed (4 changes)

View file

@ -1 +1 @@
13.1.1 13.1.2

View file

@ -560,18 +560,18 @@ GEM
json-schema (2.8.0) json-schema (2.8.0)
addressable (>= 2.4) addressable (>= 2.4)
jwt (2.1.0) jwt (2.1.0)
kaminari (1.0.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1) kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.0.1) kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-actionview (1.0.1) kaminari-actionview (1.2.1)
actionview actionview
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-activerecord (1.0.1) kaminari-activerecord (1.2.1)
activerecord activerecord
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-core (1.0.1) kaminari-core (1.2.1)
kgio (2.11.3) kgio (2.11.3)
knapsack (1.17.0) knapsack (1.17.0)
rake rake

View file

@ -1 +1 @@
13.1.1 13.1.2

View file

@ -1,7 +1,5 @@
<script> <script>
import { escape } from 'lodash'; import { GlTooltip, GlSprintf } from '@gitlab/ui';
import { GlTooltip } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
@ -11,6 +9,7 @@ export default {
ClipboardButton, ClipboardButton,
FileIcon, FileIcon,
Icon, Icon,
GlSprintf,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
@ -57,36 +56,6 @@ export default {
collapseIcon() { collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right'; return this.isExpanded ? 'chevron-down' : 'chevron-right';
}, },
errorFnText() {
return this.errorFn
? sprintf(
__(`%{spanStart}in%{spanEnd} %{errorFn}`),
{
errorFn: `<strong>${escape(this.errorFn)}</strong>`,
spanStart: `<span class="text-tertiary">`,
spanEnd: `</span>`,
},
false,
)
: '';
},
errorPositionText() {
return this.errorLine
? sprintf(
__(`%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}`),
{
errorLine: `<strong>${this.errorLine}</strong>`,
errorColumn: this.errorColumn ? `:<strong>${this.errorColumn}</strong>` : ``,
spanStart: `<span class="text-tertiary">`,
spanEnd: `</span>`,
},
false,
)
: '';
},
errorInfo() {
return `${this.errorFnText} ${this.errorPositionText}`;
},
}, },
methods: { methods: {
isHighlighted(lineNum) { isHighlighted(lineNum) {
@ -132,7 +101,27 @@ export default {
:text="filePath" :text="filePath"
css-class="btn-default btn-transparent btn-clipboard position-static" css-class="btn-default btn-transparent btn-clipboard position-static"
/> />
<span v-html="errorInfo"></span>
<gl-sprintf v-if="errorFn" :message="__('%{spanStart}in%{spanEnd} %{errorFn}')">
<template #span="{content}">
<span class="gl-text-gray-400">{{ content }}&nbsp;</span>
</template>
<template #errorFn>
<strong>{{ errorFn }}&nbsp;</strong>
</template>
</gl-sprintf>
<gl-sprintf :message="__('%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}')">
<template #span="{content}">
<span class="gl-text-gray-400">{{ content }}&nbsp;</span>
</template>
<template #errorLine>
<strong>{{ errorLine }}</strong>
</template>
<template #errorColumn>
<strong v-if="errorColumn">:{{ errorColumn }}</strong>
</template>
</gl-sprintf>
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@
* any changes done to the haml need to be reflected here. * any changes done to the haml need to be reflected here.
*/ */
import { escape, isNumber } from 'lodash'; import { escape, isNumber } from 'lodash';
import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui';
import { import {
dateInWords, dateInWords,
formatDate, formatDate,
@ -20,10 +20,14 @@ import Icon from '~/vue_shared/components/icon.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
export default { export default {
i18n: {
openedAgo: __('opened %{timeAgoString} by %{user}'),
},
components: { components: {
Icon, Icon,
IssueAssignees, IssueAssignees,
GlLink, GlLink,
GlSprintf,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
@ -98,23 +102,21 @@ export default {
} }
return __('Milestone'); return __('Milestone');
}, },
openedAgoByString() { issuableAuthor() {
const { author, created_at } = this.issuable; return this.issuable.author;
return sprintf(
__('opened %{timeAgoString} by %{user}'),
{
timeAgoString: escape(getTimeago().format(created_at)),
user: `<a href="${escape(author.web_url)}"
data-user-id=${escape(author.id)}
data-username=${escape(author.username)}
data-name=${escape(author.name)}
data-avatar-url="${escape(author.avatar_url)}">
${escape(author.name)}
</a>`,
}, },
false, issuableCreatedAt() {
); return getTimeago().format(this.issuable.created_at);
},
popoverDataAttrs() {
const { id, username, name, avatar_url } = this.issuableAuthor;
return {
'data-user-id': id,
'data-username': username,
'data-name': name,
'data-avatar-url': avatar_url,
};
}, },
referencePath() { referencePath() {
return this.issuable.references.relative; return this.issuable.references.relative;
@ -160,7 +162,7 @@ export default {
mounted() { mounted() {
// TODO: Refactor user popover to use its own component instead of // TODO: Refactor user popover to use its own component instead of
// spawning event listeners on Vue-rendered elements. // spawning event listeners on Vue-rendered elements.
initUserPopovers([this.$refs.openedAgoByContainer.querySelector('a')]); initUserPopovers([this.$refs.openedAgoByContainer.$el]);
}, },
methods: { methods: {
labelStyle(label) { labelStyle(label) {
@ -221,17 +223,30 @@ export default {
></i> ></i>
<gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link> <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link>
</span> </span>
<span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">{{ <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">
issuable.task_status {{ issuable.task_status }}
}}</span> </span>
</div> </div>
<div class="issuable-info"> <div class="issuable-info">
<span class="js-ref-path">{{ referencePath }}</span> <span class="js-ref-path">{{ referencePath }}</span>
<span class="d-none d-sm-inline-block mr-1"> <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1">
&middot; &middot;
<span ref="openedAgoByContainer" v-html="openedAgoByString"></span> <gl-sprintf :message="$options.i18n.openedAgo">
<template #timeAgoString>
<span>{{ issuableCreatedAt }}</span>
</template>
<template #user>
<gl-link
ref="openedAgoByContainer"
v-bind="popoverDataAttrs"
:href="issuableAuthor.web_url"
>
{{ issuableAuthor.name }}
</gl-link>
</template>
</gl-sprintf>
</span> </span>
<gl-link <gl-link

View file

@ -58,7 +58,7 @@ module WikiActions
render 'shared/wikis/show' render 'shared/wikis/show'
elsif file_blob elsif file_blob
send_blob(wiki.repository, file_blob, allow_caching: container.public?) send_blob(wiki.repository, file_blob)
elsif show_create_form? elsif show_create_form?
# Assign a title to the WikiPage unless `id` is a randomly generated slug from #new # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
title = params[:id] unless params[:random_title].present? title = params[:id] unless params[:random_title].present?

View file

@ -34,6 +34,18 @@ class Groups::ApplicationController < ApplicationController
end end
end end
def authorize_create_deploy_token!
unless can?(current_user, :create_deploy_token, group)
return render_404
end
end
def authorize_destroy_deploy_token!
unless can?(current_user, :destroy_deploy_token, group)
return render_404
end
end
def authorize_admin_group_member! def authorize_admin_group_member!
unless can?(current_user, :admin_group_member, group) unless can?(current_user, :admin_group_member, group)
return render_403 return render_403

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Groups::DeployTokensController < Groups::ApplicationController class Groups::DeployTokensController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_destroy_deploy_token!
def revoke def revoke
@token = @group.deploy_tokens.find(params[:id]) @token = @group.deploy_tokens.find(params[:id])

View file

@ -4,7 +4,7 @@ module Groups
module Settings module Settings
class RepositoryController < Groups::ApplicationController class RepositoryController < Groups::ApplicationController
skip_cross_project_access_check :show skip_cross_project_access_check :show
before_action :authorize_admin_group! before_action :authorize_create_deploy_token!
before_action :define_deploy_token_variables before_action :define_deploy_token_variables
before_action do before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @group) push_frontend_feature_flag(:ajax_new_deploy_token, @group)

View file

@ -33,6 +33,8 @@ class EventsFinder
end end
def execute def execute
return Event.none if cannot_access_private_profile?
events = get_events events = get_events
events = by_current_user_access(events) events = by_current_user_access(events)
@ -103,6 +105,10 @@ class EventsFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def cannot_access_private_profile?
source.is_a?(User) && !Ability.allowed?(current_user, :read_user_profile, source)
end
def sort(events) def sort(events)
return events unless params[:sort] return events unless params[:sort]

View file

@ -73,9 +73,12 @@ class Group < Namespace
validates :variables, variable_duplicates: true validates :variables, variable_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :name, validates :name,
html_safety: true,
format: { with: Gitlab::Regex.group_name_regex, format: { with: Gitlab::Regex.group_name_regex,
message: Gitlab::Regex.group_name_regex_message }, if: :name_changed? message: Gitlab::Regex.group_name_regex_message },
if: :name_changed?
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }

View file

@ -518,7 +518,7 @@ class MergeRequest < ApplicationRecord
participants << merge_user participants << merge_user
end end
participants participants.select { |participant| Ability.allowed?(participant, :read_merge_request, self) }
end end
def first_commit def first_commit

View file

@ -98,9 +98,7 @@ class GroupPolicy < BasePolicy
enable :create_cluster enable :create_cluster
enable :update_cluster enable :update_cluster
enable :admin_cluster enable :admin_cluster
enable :destroy_deploy_token
enable :read_deploy_token enable :read_deploy_token
enable :create_deploy_token
end end
rule { owner }.policy do rule { owner }.policy do
@ -112,6 +110,8 @@ class GroupPolicy < BasePolicy
enable :set_note_created_at enable :set_note_created_at
enable :set_emails_disabled enable :set_emails_disabled
enable :update_default_branch_protection enable :update_default_branch_protection
enable :create_deploy_token
enable :destroy_deploy_token
end end
rule { can?(:read_nested_project_resources) }.policy do rule { can?(:read_nested_project_resources) }.policy do

View file

@ -0,0 +1,72 @@
# frozen_string_literal: true
module Snippets
class RepositoryValidationService
attr_reader :current_user, :snippet, :repository
RepositoryValidationError = Class.new(StandardError)
def initialize(user, snippet)
@current_user = user
@snippet = snippet
@repository = snippet.repository
end
def execute
if snippet.nil?
return service_response_error('No snippet found.', 404)
end
check_branch_count!
check_branch_name_default!
check_tag_count!
check_file_count!
check_size!
ServiceResponse.success(message: 'Valid snippet repository.')
rescue RepositoryValidationError => e
ServiceResponse.error(message: "Error: #{e.message}", http_status: 400)
end
private
def check_branch_count!
return if repository.branch_count == 1
raise RepositoryValidationError, _('Repository has more than one branch.')
end
def check_branch_name_default!
branches = repository.branch_names
return if branches.first == Gitlab::Checks::SnippetCheck::DEFAULT_BRANCH
raise RepositoryValidationError, _('Repository has an invalid default branch name.')
end
def check_tag_count!
return if repository.tag_count == 0
raise RepositoryValidationError, _('Repository has tags.')
end
def check_file_count!
file_count = repository.ls_files(nil).size
limit = Snippet.max_file_limit(current_user)
if file_count > limit
raise RepositoryValidationError, _('Repository files count over the limit')
end
if file_count == 0
raise RepositoryValidationError, _('Repository must contain at least 1 file.')
end
end
def check_size!
return unless snippet.repository_size_checker.above_size_limit?
raise RepositoryValidationError, _('Repository size is above the limit.')
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
# HtmlSafetyValidator
#
# Validates that a value does not contain HTML
# or other unsafe content that could lead to XSS.
# Relies on Rails HTML Sanitizer:
# https://github.com/rails/rails-html-sanitizer
#
# Example:
#
# class Group < ActiveRecord::Base
# validates :name, presence: true, html_safety: true
# end
#
class HtmlSafetyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? || safe_value?(value)
record.errors.add(attribute, self.class.error_message)
end
def self.error_message
_("cannot contain HTML/XML tags, including any word between angle brackets (<,>).")
end
private
# The `FullSanitizer` encodes ampersands as the HTML entity name.
# This isn't particularly necessary for preventing XSS so the ampersand
# is pre-encoded to avoid it being flagged in the comparison.
def safe_value?(text)
pre_encoded_text = text.gsub('&', '&amp;')
Rails::Html::FullSanitizer.new.sanitize(pre_encoded_text) == pre_encoded_text
end
end

View file

@ -57,7 +57,7 @@
- @repos.each do |repo| - @repos.each do |repo|
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } } %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } }
%td %td
= link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target %td.import-target
%fieldset.row %fieldset.row
.input-group .input-group
@ -78,7 +78,7 @@
- @incompatible_repos.each do |repo| - @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}" } %tr{ id: "repo_#{repo.project_key}___#{repo.slug}" }
%td %td
= link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target %td.import-target
%td.import-actions-job-status %td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label badge-danger' = label_tag 'Incompatible Project', nil, class: 'label badge-danger'

View file

@ -32,7 +32,7 @@
.note-header-info .note-header-info
%a{ href: user_path(note.author) } %a{ href: user_path(note.author) }
%span.note-header-author-name.bold %span.note-header-author-name.bold
= sanitize(note.author.name) = note.author.name
= user_status(note.author) = user_status(note.author)
%span.note-headline-light %span.note-headline-light
= note.author.to_reference = note.author.to_reference

View file

@ -1,4 +1,5 @@
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
Rails.application.config.action_dispatch.use_cookies_with_metadata = true Rails.application.config.action_dispatch.use_cookies_with_metadata = true
Rails.application.config.action_dispatch.cookies_serializer = :hybrid Rails.application.config.action_dispatch.cookies_serializer =
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json

View file

@ -136,7 +136,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Group deploy tokens ## Group deploy tokens
These endpoints require group maintainer access or higher. Group maintainers and owners can list group deploy
tokens. Only group owners can create and delete group deploy tokens.
### List group deploy tokens ### List group deploy tokens

View file

@ -254,6 +254,8 @@ group.
| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) | | Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
| Edit group settings | | | | | ✓ | | Edit group settings | | | | | ✓ |
| Manage group level CI/CD variables | | | | | ✓ | | Manage group level CI/CD variables | | | | | ✓ |
| List group deploy tokens | | | | ✓ | ✓ |
| Create/Delete group deploy tokens | | | | | ✓ |
| Manage group members | | | | | ✓ | | Manage group members | | | | | ✓ |
| Delete group | | | | | ✓ | | Delete group | | | | | ✓ |
| Delete group epic **(ULTIMATE)** | | | | | ✓ | | Delete group epic **(ULTIMATE)** | | | | | ✓ |

View file

@ -4,6 +4,10 @@ module API
class ImportGithub < Grape::API class ImportGithub < Grape::API
rescue_from Octokit::Unauthorized, with: :provider_unauthorized rescue_from Octokit::Unauthorized, with: :provider_unauthorized
before do
forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('github')
end
helpers do helpers do
def client def client
@client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options)

View file

@ -14,8 +14,8 @@ module API
"#{issuable_name}_iid".to_sym "#{issuable_name}_iid".to_sym
end end
def update_issuable_key def admin_issuable_key
"update_#{issuable_name}".to_sym "admin_#{issuable_name}".to_sym
end end
def read_issuable_key def read_issuable_key
@ -60,7 +60,7 @@ module API
requires :duration, type: String, desc: 'The duration to be parsed' requires :duration, type: String, desc: 'The duration to be parsed'
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
@ -71,7 +71,7 @@ module API
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(time_estimate: 0) update_issuable(time_estimate: 0)
@ -83,7 +83,7 @@ module API
requires :duration, type: String, desc: 'The duration to be parsed' requires :duration, type: String, desc: 'The duration to be parsed'
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
update_issuable(spend_time: { update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
@ -96,7 +96,7 @@ module API
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(spend_time: { duration: :reset, user_id: current_user.id }) update_issuable(spend_time: { duration: :reset, user_id: current_user.id })

View file

@ -253,7 +253,7 @@ module Banzai
object_parent_type = parent.is_a?(Group) ? :group : :project object_parent_type = parent.is_a?(Group) ? :group : :project
{ {
original: text, original: escape_html_entities(text),
link: link_content, link: link_content,
link_reference: link_reference, link_reference: link_reference,
object_parent_type => parent.id, object_parent_type => parent.id,

View file

@ -38,7 +38,7 @@ module Banzai
private private
def unescape_and_scrub_uri(uri) def unescape_and_scrub_uri(uri)
Addressable::URI.unescape(uri).scrub Addressable::URI.unescape(uri).scrub.delete("\0")
end end
end end
end end

View file

@ -3,7 +3,7 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
class SnippetRepoRestorer < RepoRestorer class SnippetRepoRestorer < RepoRestorer
attr_reader :snippet attr_reader :snippet, :user
SnippetRepositoryError = Class.new(StandardError) SnippetRepositoryError = Class.new(StandardError)
@ -33,6 +33,16 @@ module Gitlab
def create_repository_from_bundle def create_repository_from_bundle
repository.create_from_bundle(path_to_bundle) repository.create_from_bundle(path_to_bundle)
snippet.track_snippet_repository(repository.storage) snippet.track_snippet_repository(repository.storage)
response = Snippets::RepositoryValidationService.new(user, snippet).execute
if response.error?
repository.remove
snippet.snippet_repository.delete
snippet.repository.expire_exists_cache
raise SnippetRepositoryError, _("Invalid repository bundle for snippet with id %{snippet_id}") % { snippet_id: snippet.id }
end
end end
def create_repository_from_db def create_repository_from_db

View file

@ -3,7 +3,7 @@
module Gitlab module Gitlab
module MarkdownCache module MarkdownCache
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 21 CACHE_COMMONMARK_VERSION = 23
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)

View file

@ -12299,6 +12299,9 @@ msgstr ""
msgid "Invalid query" msgid "Invalid query"
msgstr "" msgstr ""
msgid "Invalid repository bundle for snippet with id %{snippet_id}"
msgstr ""
msgid "Invalid repository path" msgid "Invalid repository path"
msgstr "" msgstr ""
@ -18943,15 +18946,33 @@ msgstr ""
msgid "Repository cleanup has started. You will receive an email once the cleanup operation is complete." msgid "Repository cleanup has started. You will receive an email once the cleanup operation is complete."
msgstr "" msgstr ""
msgid "Repository files count over the limit"
msgstr ""
msgid "Repository has an invalid default branch name."
msgstr ""
msgid "Repository has more than one branch."
msgstr ""
msgid "Repository has no locks." msgid "Repository has no locks."
msgstr "" msgstr ""
msgid "Repository has tags."
msgstr ""
msgid "Repository maintenance" msgid "Repository maintenance"
msgstr "" msgstr ""
msgid "Repository mirroring" msgid "Repository mirroring"
msgstr "" msgstr ""
msgid "Repository must contain at least 1 file."
msgstr ""
msgid "Repository size is above the limit."
msgstr ""
msgid "Repository static objects" msgid "Repository static objects"
msgstr "" msgstr ""
@ -26467,6 +26488,9 @@ msgstr ""
msgid "cannot block others" msgid "cannot block others"
msgstr "" msgstr ""
msgid "cannot contain HTML/XML tags, including any word between angle brackets (<,>)."
msgstr ""
msgid "cannot include leading slash or directory traversal." msgid "cannot include leading slash or directory traversal."
msgstr "" msgstr ""

View file

@ -126,7 +126,7 @@
"string-hash": "1.1.3", "string-hash": "1.1.3",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"svg4everybody": "2.1.9", "svg4everybody": "2.1.9",
"swagger-ui-dist": "^3.24.3", "swagger-ui-dist": "^3.26.2",
"three": "^0.84.0", "three": "^0.84.0",
"three-orbit-controls": "^82.1.0", "three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4", "three-stl-loader": "^1.0.4",
@ -152,7 +152,7 @@
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-stats-plugin": "^0.3.1", "webpack-stats-plugin": "^0.3.1",
"worker-loader": "^2.0.0", "worker-loader": "^2.0.0",
"xterm": "^3.5.0" "xterm": "3.14.5"
}, },
"devDependencies": { "devDependencies": {
"acorn": "^6.3.0", "acorn": "^6.3.0",

View file

@ -5,15 +5,17 @@ require 'spec_helper'
RSpec.describe 'Comments on personal snippets', :js do RSpec.describe 'Comments on personal snippets', :js do
include NoteInteractionHelpers include NoteInteractionHelpers
let!(:user) { create(:user) } let_it_be(:snippet) { create(:personal_snippet, :public) }
let!(:snippet) { create(:personal_snippet, :public) } let_it_be(:other_note) { create(:note_on_personal_snippet) }
let(:user_name) { 'Test User' }
let!(:user) { create(:user, name: user_name) }
let!(:snippet_notes) do let!(:snippet_notes) do
[ [
create(:note_on_personal_snippet, noteable: snippet, author: user), create(:note_on_personal_snippet, noteable: snippet, author: user),
create(:note_on_personal_snippet, noteable: snippet) create(:note_on_personal_snippet, noteable: snippet)
] ]
end end
let!(:other_note) { create(:note_on_personal_snippet) }
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
@ -56,6 +58,26 @@ RSpec.describe 'Comments on personal snippets', :js do
expect(page).to show_user_status(status) expect(page).to show_user_status(status)
end end
end end
it 'shows the author name' do
visit snippet_path(snippet)
within("#note_#{snippet_notes[0].id}") do
expect(page).to have_content(user_name)
end
end
context 'when the author name contains HTML' do
let(:user_name) { '<h1><a href="https://bad.link/malicious.exe" class="evil">Fake Content<img class="fake-icon" src="image.png"></a></h1>' }
it 'renders the name as plain text' do
visit snippet_path(snippet)
content = find("#note_#{snippet_notes[0].id} .note-header-author-name").text
expect(content).to eq user_name
end
end
end end
context 'when submitting a note' do context 'when submitting a note' do

View file

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe EventsFinder do RSpec.describe EventsFinder do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:private_user) { create(:user, private_profile: true) }
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
let(:project1) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } let(:project1) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
@ -57,6 +58,12 @@ RSpec.describe EventsFinder do
expect(events).to be_empty expect(events).to be_empty
end end
it 'returns nothing when the target profile is private' do
events = described_class.new(source: private_user, current_user: other_user).execute
expect(events).to be_empty
end
end end
describe 'wiki events feature flag' do describe 'wiki events feature flag' do

View file

@ -1,8 +1,10 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue'; import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { trimText } from 'helpers/text_helper';
describe('Stacktrace Entry', () => { describe('Stacktrace Entry', () => {
let wrapper; let wrapper;
@ -21,6 +23,9 @@ describe('Stacktrace Entry', () => {
errorLine: 24, errorLine: 24,
...props, ...props,
}, },
stubs: {
GlSprintf,
},
}); });
} }
@ -53,7 +58,7 @@ describe('Stacktrace Entry', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 }; const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(wrapper.find(Icon).exists()).toBe(false); expect(wrapper.find(Icon).exists()).toBe(false);
expect(findFileHeaderContent()).toContain( expect(trimText(findFileHeaderContent())).toContain(
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`, `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
); );
}); });
@ -61,17 +66,17 @@ describe('Stacktrace Entry', () => {
it('should render only lineNo:columnNO when there is no errorFn ', () => { it('should render only lineNo:columnNO when there is no errorFn ', () => {
const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 }; const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(findFileHeaderContent()).not.toContain(`in ${extraInfo.errorFn}`); const fileHeaderContent = trimText(findFileHeaderContent());
expect(findFileHeaderContent()).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`); expect(fileHeaderContent).not.toContain(`in ${extraInfo.errorFn}`);
expect(fileHeaderContent).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`);
}); });
it('should render only lineNo when there is no errorColumn ', () => { it('should render only lineNo when there is no errorColumn ', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null }; const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(findFileHeaderContent()).toContain( const fileHeaderContent = trimText(findFileHeaderContent());
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`, expect(fileHeaderContent).toContain(`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`);
); expect(fileHeaderContent).not.toContain(`:${extraInfo.errorColumn}`);
expect(findFileHeaderContent()).not.toContain(`:${extraInfo.errorColumn}`);
}); });
}); });
}); });

View file

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
@ -44,6 +44,10 @@ describe('Issuable component', () => {
baseUrl: TEST_BASE_URL, baseUrl: TEST_BASE_URL,
...props, ...props,
}, },
stubs: {
'gl-sprintf': GlSprintf,
'gl-link': '<a><slot></slot></a>',
},
}); });
}; };
@ -66,12 +70,12 @@ describe('Issuable component', () => {
const findConfidentialIcon = () => wrapper.find('.fa-eye-slash'); const findConfidentialIcon = () => wrapper.find('.fa-eye-slash');
const findTaskStatus = () => wrapper.find('.task-status'); const findTaskStatus = () => wrapper.find('.task-status');
const findOpenedAgoContainer = () => wrapper.find({ ref: 'openedAgoByContainer' }); const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]');
const findMilestone = () => wrapper.find('.js-milestone'); const findMilestone = () => wrapper.find('.js-milestone');
const findMilestoneTooltip = () => findMilestone().attributes('title'); const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date'); const findDueDate = () => wrapper.find('.js-due-date');
const findLabelContainer = () => wrapper.find('.js-labels'); const findLabelContainer = () => wrapper.find('.js-labels');
const findLabelLinks = () => findLabelContainer().findAll(GlLink); const findLabelLinks = () => findLabelContainer().findAll('a');
const findWeight = () => wrapper.find('.js-weight'); const findWeight = () => wrapper.find('.js-weight');
const findAssignees = () => wrapper.find(IssueAssignees); const findAssignees = () => wrapper.find(IssueAssignees);
const findMergeRequestsCount = () => wrapper.find('.js-merge-requests'); const findMergeRequestsCount = () => wrapper.find('.js-merge-requests');
@ -86,7 +90,7 @@ describe('Issuable component', () => {
factory(); factory();
expect(initUserPopovers).toHaveBeenCalledWith([findOpenedAgoContainer().find('a').element]); expect(initUserPopovers).toHaveBeenCalledWith([wrapper.vm.$refs.openedAgoByContainer.$el]);
}); });
}); });
@ -135,7 +139,7 @@ describe('Issuable component', () => {
}); });
it('renders fuzzy opened date and author', () => { it('renders fuzzy opened date and author', () => {
expect(trimText(findOpenedAgoContainer().text())).toEqual( expect(trimText(findOpenedAgoContainer().text())).toContain(
`opened 1 month ago by ${TEST_USER_NAME}`, `opened 1 month ago by ${TEST_USER_NAME}`,
); );
}); });

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Cookies serializer initializer' do
def load_initializer
load Rails.root.join('config/initializers/cookies_serializer.rb')
end
subject { Rails.application.config.action_dispatch.cookies_serializer }
it 'uses JSON serializer by default' do
load_initializer
expect(subject).to eq(:json)
end
it 'uses the unsafe hybrid serializer when the environment variables is set' do
stub_env('USE_UNSAFE_HYBRID_COOKIES', 'true')
load_initializer
expect(subject).to eq(:hybrid)
end
end

View file

@ -20,6 +20,18 @@ describe Banzai::Filter::AbstractReferenceFilter do
end end
end end
describe '#data_attributes_for' do
let_it_be(:issue) { create(:issue, project: project) }
it 'is not an XSS vector' do
allow(described_class).to receive(:object_class).and_return(Issue)
data_attributes = filter.data_attributes_for('xss &lt;img onerror=alert(1) src=x&gt;', project, issue, link_content: true)
expect(data_attributes[:original]).to eq('xss &amp;lt;img onerror=alert(1) src=x&amp;gt;')
end
end
describe '#parent_per_reference' do describe '#parent_per_reference' do
it 'returns a Hash containing projects grouped per parent paths' do it 'returns a Hash containing projects grouped per parent paths' do
expect(filter).to receive(:references_per_parent) expect(filter).to receive(:references_per_parent)

View file

@ -229,6 +229,7 @@ describe Banzai::Filter::UploadLinkFilter do
'invalid UTF-8 byte sequences' | '%FF' 'invalid UTF-8 byte sequences' | '%FF'
'garbled path' | 'open(/var/tmp/):%20/location%0Afrom:%20/test' 'garbled path' | 'open(/var/tmp/):%20/location%0Afrom:%20/test'
'whitespace' | "d18213acd3732630991986120e167e3d/Landscape_8.jpg\nand more" 'whitespace' | "d18213acd3732630991986120e167e3d/Landscape_8.jpg\nand more"
'null byte' | "%00"
end end
with_them do with_them do

View file

@ -24,7 +24,7 @@ describe Banzai::Pipeline::FullPipeline do
it 'escapes the data-original attribute on a reference' do it 'escapes the data-original attribute on a reference' do
markdown = %Q{[">bad things](#{issue.to_reference})} markdown = %Q{[">bad things](#{issue.to_reference})}
result = described_class.to_html(markdown, project: project) result = described_class.to_html(markdown, project: project)
expect(result).to include(%{data-original='\"&gt;bad things'}) expect(result).to include(%{data-original='\"&amp;gt;bad things'})
end end
end end

View file

@ -4,9 +4,9 @@ require 'spec_helper'
describe Gitlab::ImportExport::SnippetRepoRestorer do describe Gitlab::ImportExport::SnippetRepoRestorer do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, project: project, author: user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, project: project, author: user) }
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) } let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) }
let(:restorer) do let(:restorer) do
@ -57,20 +57,29 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
it_behaves_like 'no bundle file present' it_behaves_like 'no bundle file present'
end end
context 'when the snippet bundle exists' do context 'when the snippet repository bundle exists' do
let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project) } let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project, author: user) }
let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) }
let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") } let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") }
let(:result) { exporter.save } let(:result) { exporter.save }
let(:repository) { snippet.repository }
before do before do
expect(exporter.save).to be_truthy expect(exporter.save).to be_truthy
end end
context 'when it is valid' do
before do
allow(repository).to receive(:branch_count).and_return(1)
allow(repository).to receive(:tag_count).and_return(0)
allow(repository).to receive(:branch_names).and_return(['master'])
allow(repository).to receive(:ls_files).and_return(['foo'])
end
it 'creates the repository from the bundle' do it 'creates the repository from the bundle' do
expect(snippet.repository_exists?).to be_falsey expect(snippet.repository_exists?).to be_falsey
expect(snippet.snippet_repository).to be_nil expect(snippet.snippet_repository).to be_nil
expect(snippet.repository).to receive(:create_from_bundle).and_call_original expect(repository).to receive(:create_from_bundle).and_call_original
expect(restorer.restore).to be_truthy expect(restorer.restore).to be_truthy
expect(snippet.repository_exists?).to be_truthy expect(snippet.repository_exists?).to be_truthy
@ -78,12 +87,33 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
end end
it 'sets same shard in snippet repository as in the repository storage' do it 'sets same shard in snippet repository as in the repository storage' do
expect(snippet).to receive(:repository_storage).and_return('picked') expect(repository).to receive(:storage).and_return('picked')
expect(snippet.repository).to receive(:create_from_bundle) expect(repository).to receive(:create_from_bundle)
restorer.restore
expect(restorer.restore).to be_truthy
expect(snippet.snippet_repository.shard_name).to eq 'picked' expect(snippet.snippet_repository.shard_name).to eq 'picked'
end end
end end
context 'when it is invalid' do
it 'returns false and deletes the repository from disk and the database' do
gitlab_shell = Gitlab::Shell.new
shard_name = snippet.repository.shard
path = snippet.disk_path + '.git'
error_response = ServiceResponse.error(message: 'Foo', http_status: 400)
allow_next_instance_of(Snippets::RepositoryValidationService) do |instance|
allow(instance).to receive(:execute).and_return(error_response)
end
aggregate_failures do
expect(restorer.restore).to be false
expect(shared.errors.first).to match(/Invalid repository bundle/)
expect(snippet.repository_exists?).to eq false
expect(snippet.reload.snippet_repository).to be_nil
expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false
end
end
end
end
end end

View file

@ -38,6 +38,7 @@ describe Gitlab::ImportExport::SnippetsRepoRestorer do
expect(snippet1.repository_exists?).to be false expect(snippet1.repository_exists?).to be false
expect(snippet2.repository_exists?).to be false expect(snippet2.repository_exists?).to be false
allow_any_instance_of(Snippets::RepositoryValidationService).to receive(:execute).and_return(ServiceResponse.success)
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet1, path_to_bundle: bundle_path(snippet1))).and_call_original expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet1, path_to_bundle: bundle_path(snippet1))).and_call_original
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet2, path_to_bundle: bundle_path(snippet2))).and_call_original expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet2, path_to_bundle: bundle_path(snippet2))).and_call_original
expect(restorer.restore).to be_truthy expect(restorer.restore).to be_truthy

View file

@ -3655,7 +3655,7 @@ describe MergeRequest do
describe '#merge_participants' do describe '#merge_participants' do
it 'contains author' do it 'contains author' do
expect(subject.merge_participants).to eq([subject.author]) expect(subject.merge_participants).to contain_exactly(subject.author)
end end
describe 'when merge_when_pipeline_succeeds? is true' do describe 'when merge_when_pipeline_succeeds? is true' do
@ -3669,8 +3669,20 @@ describe MergeRequest do
author: user) author: user)
end end
context 'author is not a project member' do
it 'is empty' do
expect(subject.merge_participants).to be_empty
end
end
context 'author is a project member' do
before do
subject.project.team.add_reporter(user)
end
it 'contains author only' do it 'contains author only' do
expect(subject.merge_participants).to eq([subject.author]) expect(subject.merge_participants).to contain_exactly(subject.author)
end
end end
end end
@ -3683,8 +3695,24 @@ describe MergeRequest do
merge_user: merge_user) merge_user: merge_user)
end end
before do
subject.project.team.add_reporter(subject.author)
end
context 'merge user is not a member' do
it 'contains author only' do
expect(subject.merge_participants).to contain_exactly(subject.author)
end
end
context 'both author and merge users are project members' do
before do
subject.project.team.add_reporter(merge_user)
end
it 'contains author and merge user' do it 'contains author and merge user' do
expect(subject.merge_participants).to eq([subject.author, merge_user]) expect(subject.merge_participants).to contain_exactly(subject.author, merge_user)
end
end end
end end
end end

View file

@ -204,7 +204,7 @@ describe API::DeployTokens do
end end
context 'deploy token creation' do context 'deploy token creation' do
shared_examples 'creating a deploy token' do |entity, unauthenticated_response| shared_examples 'creating a deploy token' do |entity, unauthenticated_response, authorized_role|
let(:expires_time) { 1.year.from_now } let(:expires_time) { 1.year.from_now }
let(:params) do let(:params) do
{ {
@ -231,9 +231,9 @@ describe API::DeployTokens do
it { is_expected.to have_gitlab_http_status(:forbidden) } it { is_expected.to have_gitlab_http_status(:forbidden) }
end end
context 'when authenticated as maintainer' do context "when authenticated as #{authorized_role}" do
before do before do
send(entity).add_maintainer(user) send(entity).send("add_#{authorized_role}", user)
end end
it 'creates the deploy token' do it 'creates the deploy token' do
@ -282,7 +282,7 @@ describe API::DeployTokens do
response response
end end
it_behaves_like 'creating a deploy token', :project, :not_found it_behaves_like 'creating a deploy token', :project, :not_found, :maintainer
end end
describe 'POST /groups/:id/deploy_tokens' do describe 'POST /groups/:id/deploy_tokens' do
@ -291,7 +291,17 @@ describe API::DeployTokens do
response response
end end
it_behaves_like 'creating a deploy token', :group, :forbidden it_behaves_like 'creating a deploy token', :group, :forbidden, :owner
context 'when authenticated as maintainer' do
before do
group.add_maintainer(user)
end
let(:params) { { name: 'test', scopes: ['read_repository'] } }
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
end end
end end
@ -320,6 +330,14 @@ describe API::DeployTokens do
group.add_maintainer(user) group.add_maintainer(user)
end end
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
context 'when authenticated as owner' do
before do
group.add_owner(user)
end
it 'calls the deploy token destroy service' do it 'calls the deploy token destroy service' do
expect(::Groups::DeployTokens::DestroyService).to receive(:new) expect(::Groups::DeployTokens::DestroyService).to receive(:new)
.with(group, user, token_id: group_deploy_token.id) .with(group, user, token_id: group_deploy_token.id)

View file

@ -192,6 +192,19 @@ describe API::Events do
end end
end end
context 'when target users profile is private' do
it 'returns no events' do
user.update!(private_profile: true)
private_project.add_developer(non_member)
get api("/users/#{user.username}/events", non_member)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to eq([])
end
end
context 'when scope is passed' do context 'when scope is passed' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns no user events' do it 'returns no user events' do

View file

@ -26,6 +26,18 @@ describe API::ImportGithub do
end end
end end
it 'rejects requests when Github Importer is disabled' do
stub_application_setting(import_sources: nil)
post api("/import/github", user), params: {
target_namespace: user.namespace_path,
personal_access_token: token,
repo_id: non_existing_record_id
}
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns 201 response when the project is imported successfully' do it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator) allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)

View file

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
describe Snippets::RepositoryValidationService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :empty_repo, author: user) }
let(:repository) { snippet.repository }
let(:service) { described_class.new(user, snippet) }
subject { service.execute }
before do
allow(repository).to receive(:branch_count).and_return(1)
allow(repository).to receive(:ls_files).and_return(['foo'])
allow(repository).to receive(:branch_names).and_return(['master'])
end
it 'returns error when the repository has more than one branch' do
allow(repository).to receive(:branch_count).and_return(2)
expect(subject).to be_error
expect(subject.message).to match /Repository has more than one branch/
end
it 'returns error when existing branch name is not the default one' do
allow(repository).to receive(:branch_names).and_return(['foo'])
expect(subject).to be_error
expect(subject.message).to match /Repository has an invalid default branch name/
end
it 'returns error when the repository has tags' do
allow(repository).to receive(:tag_count).and_return(1)
expect(subject).to be_error
expect(subject.message).to match /Repository has tags/
end
it 'returns error when the repository has more file than the limit' do
limit = Snippet.max_file_limit(user) + 1
files = Array.new(limit) { FFaker::Filesystem.file_name }
allow(repository).to receive(:ls_files).and_return(files)
expect(subject).to be_error
expect(subject.message).to match /Repository files count over the limit/
end
it 'returns error when the repository has no files' do
allow(repository).to receive(:ls_files).and_return([])
expect(subject).to be_error
expect(subject.message).to match /Repository must contain at least 1 file/
end
it 'returns error when the repository size is over the limit' do
expect_any_instance_of(Gitlab::RepositorySizeChecker).to receive(:above_size_limit?).and_return(true)
expect(subject).to be_error
expect(subject.message).to match /Repository size is above the limit/
end
it 'returns success when no validation errors are raised' do
expect(subject).to be_success
end
end
end

View file

@ -158,46 +158,18 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when page is a file' do context 'when page is a file' do
include WikiHelpers include WikiHelpers
where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] }
with_them do
let(:id) { upload_file_to_wiki(container, user, file_name) } let(:id) { upload_file_to_wiki(container, user, file_name) }
context 'when file is an image' do it 'delivers the file with the correct headers' do
let(:file_name) { 'dk.png' }
it 'delivers the image' do
subject subject
expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true')
end expect(response.cache_control[:public]).to be(false)
expect(response.cache_control[:extras]).to include('no-store')
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'delivers the image' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
it_behaves_like 'project cache control headers' do
let(:project) { container }
end
end
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to sets the content response headers' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
it_behaves_like 'project cache control headers' do
let(:project) { container }
end end
end end
end end

View file

@ -4,6 +4,16 @@ RSpec.shared_examples 'an unauthorized API user' do
it { is_expected.to eq(403) } it { is_expected.to eq(403) }
end end
RSpec.shared_examples 'API user with insufficient permissions' do
context 'with non member that is the author' do
before do
issuable.update!(author: non_member) # an external author can't admin issuable
end
it_behaves_like 'an unauthorized API user'
end
end
RSpec.shared_examples 'time tracking endpoints' do |issuable_name| RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
@ -14,6 +24,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "sets the time estimate for #{issuable_name}" do it "sets the time estimate for #{issuable_name}" do
@ -53,6 +64,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "resets the time estimate for #{issuable_name}" do it "resets the time estimate for #{issuable_name}" do
@ -70,6 +82,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
end end
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "add spent time for #{issuable_name}" do it "add spent time for #{issuable_name}" do
@ -119,6 +132,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "resets spent time for #{issuable_name}" do it "resets spent time for #{issuable_name}" do

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe HtmlSafetyValidator do
let(:validator) { described_class.new(attributes: [:name]) }
let(:group) { build(:group) }
def validate(value)
validator.validate_each(group, :name, value)
end
it 'adds an error when a script is included in the name' do
validate('My group <script>evil_script</script>')
expect(group.errors[:name]).to eq([HtmlSafetyValidator.error_message])
end
it 'does not add an error when an ampersand is included in the name' do
validate('Group with 1 & 2')
expect(group.errors).to be_empty
end
end

View file

@ -11181,10 +11181,10 @@ svg4everybody@2.1.9:
resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0= integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0=
swagger-ui-dist@^3.24.3: swagger-ui-dist@^3.26.2:
version "3.24.3" version "3.26.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.24.3.tgz#99754d11b0ddd314a1a50db850acb415e4b0a0c6" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.26.2.tgz#22c700906c8911b1c9956da6c3fca371dba6219f"
integrity sha512-kB8qobP42Xazaym7sD9g5mZuRL4416VIIYZMqPEIskkzKqbPLQGEiHA3ga31bdzyzFLgr6Z797+6X1Am6zYpbg== integrity sha512-cpR3A9uEs95gGQSaIXgiTpnetIifTF1u2a0fWrnVl+HyLpCdHVgOy7FGlVD1iVkts7AE5GOiGjA7VyDNiRaNgw==
symbol-observable@^1.0.2: symbol-observable@^1.0.2:
version "1.2.0" version "1.2.0"
@ -12606,10 +12606,10 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm@^3.5.0: xterm@3.14.5:
version "3.5.0" version "3.14.5"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.5.0.tgz#ba3f464bc5730c9d259ebe62131862224db9ddcc" resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.5.tgz#c9d14e48be6873aa46fb429f22f2165557fd2dea"
integrity sha512-IpG3P3gkT0/xDPS0j3igpk92JYlUajaEHk3/EQSUeIRJmPiF2lyham3Xt/GD3o98uOrRluvowjNj0AFeYK+AXQ== integrity sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g==
y18n@^3.2.1: y18n@^3.2.1:
version "3.2.1" version "3.2.1"