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:
commit
f2ef523b6c
49 changed files with 634 additions and 7036 deletions
6858
CHANGELOG-EE.md
6858
CHANGELOG-EE.md
File diff suppressed because it is too large
Load diff
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -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)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
13.1.1
|
13.1.2
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -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
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
13.1.1
|
13.1.2
|
||||||
|
|
|
@ -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 }} </span>
|
||||||
|
</template>
|
||||||
|
<template #errorFn>
|
||||||
|
<strong>{{ errorFn }} </strong>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
|
||||||
|
<gl-sprintf :message="__('%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}')">
|
||||||
|
<template #span="{content}">
|
||||||
|
<span class="gl-text-gray-400">{{ content }} </span>
|
||||||
|
</template>
|
||||||
|
<template #errorLine>
|
||||||
|
<strong>{{ errorLine }}</strong>
|
||||||
|
</template>
|
||||||
|
<template #errorColumn>
|
||||||
|
<strong v-if="errorColumn">:{{ errorColumn }}</strong>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
·
|
·
|
||||||
<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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
72
app/services/snippets/repository_validation_service.rb
Normal file
72
app/services/snippets/repository_validation_service.rb
Normal 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
|
36
app/validators/html_safety_validator.rb
Normal file
36
app/validators/html_safety_validator.rb
Normal 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('&', '&')
|
||||||
|
Rails::Html::FullSanitizer.new.sanitize(pre_encoded_text) == pre_encoded_text
|
||||||
|
end
|
||||||
|
end
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)** | | | | | ✓ |
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
25
spec/initializers/cookies_serializer_spec.rb
Normal file
25
spec/initializers/cookies_serializer_spec.rb
Normal 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
|
|
@ -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 <img onerror=alert(1) src=x>', project, issue, link_content: true)
|
||||||
|
|
||||||
|
expect(data_attributes[:original]).to eq('xss &lt;img onerror=alert(1) src=x&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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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='\">bad things'})
|
expect(result).to include(%{data-original='\"&gt;bad things'})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
69
spec/services/snippets/repository_validation_service_spec.rb
Normal file
69
spec/services/snippets/repository_validation_service_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
24
spec/validators/html_safety_validator_spec.rb
Normal file
24
spec/validators/html_safety_validator_spec.rb
Normal 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
|
16
yarn.lock
16
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue