Merge branch 'master' of dev.gitlab.org:gitlab/gitlabhq

This commit is contained in:
Marin Jankovski 2019-07-03 11:55:42 +02:00
commit 49d689fb3c
No known key found for this signature in database
GPG key ID: 29689955693D45E8
85 changed files with 1438 additions and 332 deletions

View file

@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.0.3 (2019-06-27)
- No changes.
### Security (10 changes)
- Persist tmp snippet uploads at users.
- Gate MR head_pipeline behind read_pipeline ability.
- Fix DoS vulnerability in color validation regex.
- Expose merge requests count based on user access.
- Fix Denial of Service for comments when rendering issues/MR comments.
- Add missing authorizations in GraphQL.
- Disable Rails SQL query cache when applying service templates.
- Prevent Billion Laughs attack.
- Correctly check permissions when creating snippet notes.
- Prevent the detection of merge request templates by unauthorized users.
## 12.0.2 (2019-06-25) ## 12.0.2 (2019-06-25)
### Fixed (7 changes, 1 of them is from the community) ### Fixed (7 changes, 1 of them is from the community)
@ -555,6 +572,27 @@ entry.
- Add some frozen string to spec/**/*.rb. (gfyoung) - Add some frozen string to spec/**/*.rb. (gfyoung)
## 11.10.8 (2019-06-27)
- No changes.
### Security (10 changes)
- Fix Denial of Service for comments when rendering issues/MR comments.
- Gate MR head_pipeline behind read_pipeline ability.
- Fix DoS vulnerability in color validation regex.
- Expose merge requests count based on user access.
- Persist tmp snippet uploads at users.
- Add missing authorizations in GraphQL.
- Disable Rails SQL query cache when applying service templates.
- Prevent Billion Laughs attack.
- Correctly check permissions when creating snippet notes.
- Prevent the detection of merge request templates by unauthorized users.
### Performance (1 change)
- Add improvements to global search of issues and merge requests. !27817
## 11.10.6 (2019-06-04) ## 11.10.6 (2019-06-04)
### Fixed (7 changes, 1 of them is from the community) ### Fixed (7 changes, 1 of them is from the community)

View file

@ -42,7 +42,7 @@ module IssuableCollections
@issuables = @issuables.page(params[:page]) @issuables = @issuables.page(params[:page])
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position' @issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
@issuable_meta_data = issuable_meta_data(@issuables, collection_type) @issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
@total_pages = issuable_page_count @total_pages = issuable_page_count
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables

View file

@ -11,7 +11,7 @@ module IssuableCollectionsAction
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, collection_type) @issuable_meta_data = issuable_meta_data(@issues, collection_type, current_user)
respond_to do |format| respond_to do |format|
format.html format.html
@ -22,7 +22,7 @@ module IssuableCollectionsAction
def merge_requests def merge_requests
@merge_requests = issuables_collection.page(params[:page]) @merge_requests = issuables_collection.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type, current_user)
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables

View file

@ -203,17 +203,17 @@ module NotesActions
# These params are also sent by the client but we need to set these based on # These params are also sent by the client but we need to set these based on
# target_type and target_id because we're checking permissions based on that # target_type and target_id because we're checking permissions based on that
create_params[:noteable_type] = params[:target_type].classify create_params[:noteable_type] = noteable.class.name
case params[:target_type] case noteable
when 'commit' when Commit
create_params[:commit_id] = params[:target_id] create_params[:commit_id] = noteable.id
when 'merge_request' when MergeRequest
create_params[:noteable_id] = params[:target_id] create_params[:noteable_id] = noteable.id
# Notes on MergeRequest can have an extra `commit_id` context # Notes on MergeRequest can have an extra `commit_id` context
create_params[:commit_id] = params.dig(:note, :commit_id) create_params[:commit_id] = params.dig(:note, :commit_id)
else else
create_params[:noteable_id] = params[:target_id] create_params[:noteable_id] = noteable.id
end end
end end
end end

View file

@ -12,6 +12,11 @@ class Projects::ApplicationController < ApplicationController
helper_method :repository, :can_collaborate_with_project?, :user_access helper_method :repository, :can_collaborate_with_project?, :user_access
rescue_from Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError do |exception|
log_exception(exception)
render_404
end
private private
def project def project

View file

@ -1,7 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Projects::TemplatesController < Projects::ApplicationController class Projects::TemplatesController < Projects::ApplicationController
before_action :authenticate_user!, :get_template_class before_action :authenticate_user!
before_action :authorize_can_read_issuable!
before_action :get_template_class
def show def show
template = @template_type.find(params[:key], project) template = @template_type.find(params[:key], project)
@ -13,9 +15,20 @@ class Projects::TemplatesController < Projects::ApplicationController
private private
# User must have:
# - `read_merge_request` to see merge request templates, or
# - `read_issue` to see issue templates
#
# Note params[:template_type] has a route constraint to limit it to
# `merge_request` or `issue`
def authorize_can_read_issuable!
action = [:read_, params[:template_type]].join
authorize_action!(action)
end
def get_template_class def get_template_class
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
@template_type = template_types[params[:template_type]] @template_type = template_types[params[:template_type]]
render json: [], status: :not_found unless @template_type
end end
end end

View file

@ -298,7 +298,7 @@ class ProjectsController < Projects::ApplicationController
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
@issues = issuables_collection.page(params[:page]) @issues = issuables_collection.page(params[:page])
@collection_type = 'Issue' @collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @issuable_meta_data = issuable_meta_data(@issues, @collection_type, current_user)
end end
render :show render :show

View file

@ -5,8 +5,8 @@ class Snippets::NotesController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index] skip_before_action :authenticate_user!, only: [:index]
before_action :snippet before_action :authorize_read_snippet!, only: [:show, :index]
before_action :authorize_read_snippet!, only: [:show, :index, :create] before_action :authorize_create_note!, only: [:create]
private private
@ -33,4 +33,8 @@ class Snippets::NotesController < ApplicationController
def authorize_read_snippet! def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet) return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end end
def authorize_create_note!
access_denied! unless can?(current_user, :create_note, noteable)
end
end end

View file

@ -137,7 +137,7 @@ class SnippetsController < ApplicationController
def move_temporary_files def move_temporary_files
params[:files].each do |file| params[:files].each do |file|
FileMover.new(file, @snippet).execute FileMover.new(file, from_model: current_user, to_model: @snippet).execute
end end
end end
end end

View file

@ -41,7 +41,11 @@ class UploadsController < ApplicationController
when Note when Note
can?(current_user, :read_project, model.project) can?(current_user, :read_project, model.project)
when User when User
true # We validate the current user has enough (writing)
# access to itself when a secret is given.
# For instance, user avatars are readable by anyone,
# while temporary, user snippet uploads are not.
!secret? || can?(current_user, :update_user, model)
when Appearance when Appearance
true true
else else
@ -56,9 +60,13 @@ class UploadsController < ApplicationController
def authorize_create_access! def authorize_create_access!
return unless model return unless model
# for now we support only personal snippets comments. Only personal_snippet authorized =
# is allowed as a model to #create through routing. case model
authorized = can?(current_user, :create_note, model) when User
can?(current_user, :update_user, model)
else
can?(current_user, :create_note, model)
end
render_unauthorized unless authorized render_unauthorized unless authorized
end end
@ -75,6 +83,10 @@ class UploadsController < ApplicationController
User === model || Appearance === model User === model || Appearance === model
end end
def secret?
params[:secret].present?
end
def upload_model_class def upload_model_class
MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError) MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
module Ci module Ci
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `PipelineType` that has its own authorization
class DetailedStatusType < BaseObject class DetailedStatusType < BaseObject
graphql_name 'DetailedStatus' graphql_name 'DetailedStatus'
@ -13,5 +15,6 @@ module Types
field :text, GraphQL::STRING_TYPE, null: false field :text, GraphQL::STRING_TYPE, null: false
field :tooltip, GraphQL::STRING_TYPE, null: false, method: :status_tooltip field :tooltip, GraphQL::STRING_TYPE, null: false, method: :status_tooltip
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end

View file

@ -1,8 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is a BaseEnum through IssuableEnum, so it does not need authorization
class IssueStateEnum < IssuableStateEnum class IssueStateEnum < IssuableStateEnum
graphql_name 'IssueState' graphql_name 'IssueState'
description 'State of a GitLab issue' description 'State of a GitLab issue'
end end
# rubocop: enable Graphql/AuthorizeTypes
end end

View file

@ -4,6 +4,8 @@ module Types
class LabelType < BaseObject class LabelType < BaseObject
graphql_name 'Label' graphql_name 'Label'
authorize :read_label
field :description, GraphQL::STRING_TYPE, null: true field :description, GraphQL::STRING_TYPE, null: true
markdown_field :description_html, null: true markdown_field :description_html, null: true
field :title, GraphQL::STRING_TYPE, null: false field :title, GraphQL::STRING_TYPE, null: false

View file

@ -1,10 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is a BaseEnum through IssuableEnum, so it does not need authorization
class MergeRequestStateEnum < IssuableStateEnum class MergeRequestStateEnum < IssuableStateEnum
graphql_name 'MergeRequestState' graphql_name 'MergeRequestState'
description 'State of a GitLab merge request' description 'State of a GitLab merge request'
value 'merged' value 'merged'
end end
# rubocop: enable Graphql/AuthorizeTypes
end end

View file

@ -4,6 +4,8 @@ module Types
class MetadataType < ::Types::BaseObject class MetadataType < ::Types::BaseObject
graphql_name 'Metadata' graphql_name 'Metadata'
authorize :read_instance_metadata
field :version, GraphQL::STRING_TYPE, null: false field :version, GraphQL::STRING_TYPE, null: false
field :revision, GraphQL::STRING_TYPE, null: false field :revision, GraphQL::STRING_TYPE, null: false
end end

View file

@ -4,6 +4,8 @@ module Types
class NamespaceType < BaseObject class NamespaceType < BaseObject
graphql_name 'Namespace' graphql_name 'Namespace'
authorize :read_namespace
field :id, GraphQL::ID_TYPE, null: false field :id, GraphQL::ID_TYPE, null: false
field :name, GraphQL::STRING_TYPE, null: false field :name, GraphQL::STRING_TYPE, null: false

View file

@ -2,6 +2,8 @@
module Types module Types
module Notes module Notes
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `NoteType` that has its own authorization
class DiffPositionType < BaseObject class DiffPositionType < BaseObject
graphql_name 'DiffPosition' graphql_name 'DiffPosition'
@ -42,5 +44,6 @@ module Types
description: "The total height of the image", description: "The total height of the image",
resolve: -> (position, _args, _ctx) { position.height if position.on_image? } resolve: -> (position, _args, _ctx) { position.height if position.on_image? }
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end

View file

@ -67,14 +67,14 @@ module Types
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :namespace, Types::NamespaceType, null: false field :namespace, Types::NamespaceType, null: true
field :group, Types::GroupType, null: true field :group, Types::GroupType, null: true
field :statistics, Types::ProjectStatisticsType, field :statistics, Types::ProjectStatisticsType,
null: true, null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find } resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
field :repository, Types::RepositoryType, null: false field :repository, Types::RepositoryType, null: true
field :merge_requests, field :merge_requests,
Types::MergeRequestType.connection_type, Types::MergeRequestType.connection_type,

View file

@ -22,10 +22,7 @@ module Types
field :metadata, Types::MetadataType, field :metadata, Types::MetadataType,
null: true, null: true,
resolver: Resolvers::MetadataResolver, resolver: Resolvers::MetadataResolver,
description: 'Metadata about GitLab' do |*args| description: 'Metadata about GitLab'
authorize :read_instance_metadata
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end end

View file

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is used in `IssueType` and `MergeRequestType` both of which have their
# own authorization
class TaskCompletionStatus < BaseObject class TaskCompletionStatus < BaseObject
graphql_name 'TaskCompletionStatus' graphql_name 'TaskCompletionStatus'
description 'Completion status of tasks' description 'Completion status of tasks'
@ -8,4 +11,5 @@ module Types
field :count, GraphQL::INT_TYPE, null: false field :count, GraphQL::INT_TYPE, null: false
field :completed_count, GraphQL::INT_TYPE, null: false field :completed_count, GraphQL::INT_TYPE, null: false
end end
# rubocop: enable Graphql/AuthorizeTypes
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
module Tree module Tree
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class BlobType < BaseObject class BlobType < BaseObject
implements Types::Tree::EntryType implements Types::Tree::EntryType
@ -12,6 +14,7 @@ module Types
field :lfs_oid, GraphQL::STRING_TYPE, null: true, resolve: -> (blob, args, ctx) do field :lfs_oid, GraphQL::STRING_TYPE, null: true, resolve: -> (blob, args, ctx) do
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end
end end

View file

@ -1,10 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
module Tree module Tree
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class SubmoduleType < BaseObject class SubmoduleType < BaseObject
implements Types::Tree::EntryType implements Types::Tree::EntryType
graphql_name 'Submodule' graphql_name 'Submodule'
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
module Tree module Tree
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class TreeEntryType < BaseObject class TreeEntryType < BaseObject
implements Types::Tree::EntryType implements Types::Tree::EntryType
@ -11,5 +13,6 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true field :web_url, GraphQL::STRING_TYPE, null: true
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
module Tree module Tree
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class TreeType < BaseObject class TreeType < BaseObject
graphql_name 'Tree' graphql_name 'Tree'
@ -13,6 +15,7 @@ module Types
field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do
Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository) Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
end end
end end

View file

@ -280,7 +280,7 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status initialTaskStatus: issuable.task_status
} }
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue) data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
if parent.is_a?(Group) if parent.is_a?(Group)
data[:groupPath] = parent.path data[:groupPath] = parent.path

View file

@ -1,6 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
module SnippetsHelper module SnippetsHelper
def snippets_upload_path(snippet, user)
return unless user
if snippet&.persisted?
upload_path('personal_snippet', id: snippet.id)
else
upload_path('user', id: user.id)
end
end
def reliable_snippet_path(snippet, opts = nil) def reliable_snippet_path(snippet, opts = nil)
if snippet.project_id? if snippet.project_id?
project_snippet_path(snippet.project, snippet, opts) project_snippet_path(snippet.project, snippet, opts)

View file

@ -29,7 +29,11 @@ module Issuable
# This object is used to gather issuable meta data for displaying # This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance. # lists avoiding n+1 queries and improving performance.
IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :merge_requests_count) IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
def merge_requests_count(user = nil)
mrs_count
end
end
included do included do
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line

View file

@ -250,8 +250,8 @@ class Issue < ApplicationRecord
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
def merge_requests_count def merge_requests_count(user = nil)
merge_requests_closing_issues.count ::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end end
def labels_hook_attrs def labels_hook_attrs

View file

@ -7,11 +7,38 @@ class MergeRequestsClosingIssues < ApplicationRecord
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
validates :issue_id, presence: true validates :issue_id, presence: true
scope :with_issues, ->(ids) { where(issue_id: ids) }
scope :with_merge_requests_enabled, -> do
joins(:merge_request)
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
.where('project_features.merge_requests_access_level >= :access', access: ProjectFeature::ENABLED)
end
scope :accessible_by, ->(user) do
joins(:merge_request)
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
.where('project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)',
access: ProjectFeature::ENABLED,
authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id")
)
end
class << self class << self
def count_for_collection(ids) def count_for_collection(ids, current_user)
group(:issue_id) closing_merge_requests(ids, current_user).group(:issue_id).pluck('issue_id', 'COUNT(*) as count')
.where(issue_id: ids) end
.pluck('issue_id', 'COUNT(*) as count')
def count_for_issue(id, current_user)
closing_merge_requests(id, current_user).count
end
private
def closing_merge_requests(ids, current_user)
return with_issues(ids) if current_user&.admin?
return with_issues(ids).with_merge_requests_enabled if current_user.blank?
with_issues(ids).accessible_by(current_user)
end end
end end
end end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class RepositoryPolicy < BasePolicy
delegate { @subject.project }
end

View file

@ -1,22 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
class FileMover class FileMover
attr_reader :secret, :file_name, :model, :update_field include Gitlab::Utils::StrongMemoize
def initialize(file_path, model, update_field = :description) attr_reader :secret, :file_name, :from_model, :to_model, :update_field
def initialize(file_path, update_field = :description, from_model:, to_model:)
@secret = File.split(File.dirname(file_path)).last @secret = File.split(File.dirname(file_path)).last
@file_name = File.basename(file_path) @file_name = File.basename(file_path)
@model = model @from_model = from_model
@to_model = to_model
@update_field = update_field @update_field = update_field
end end
def execute def execute
temp_file_uploader.retrieve_from_store!(file_name)
return unless valid? return unless valid?
uploader.retrieve_from_store!(file_name)
move move
if update_markdown if update_markdown
uploader.record_upload update_upload_model
uploader.schedule_background_upload uploader.schedule_background_upload
end end
end end
@ -24,52 +31,77 @@ class FileMover
private private
def valid? def valid?
Pathname.new(temp_file_path).realpath.to_path.start_with?( if temp_file_uploader.file_storage?
(Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path Pathname.new(temp_file_path).realpath.to_path.start_with?(
) (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
)
else
temp_file_uploader.exists?
end
end end
def move def move
FileUtils.mkdir_p(File.dirname(file_path)) if temp_file_uploader.file_storage?
FileUtils.move(temp_file_path, file_path) FileUtils.mkdir_p(File.dirname(file_path))
FileUtils.move(temp_file_path, file_path)
else
uploader.copy_file(temp_file_uploader.file)
temp_file_uploader.upload.destroy!
end
end end
def update_markdown def update_markdown
updated_text = model.read_attribute(update_field) updated_text = to_model.read_attribute(update_field)
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link) .gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
model.update_attribute(update_field, updated_text) to_model.update_attribute(update_field, updated_text)
rescue rescue
revert revert
false false
end end
def update_upload_model
return unless upload = temp_file_uploader.upload
return if upload.destroyed?
upload.update!(model: to_model)
end
def temp_file_path def temp_file_path
return @temp_file_path if @temp_file_path strong_memoize(:temp_file_path) do
temp_file_uploader.file.path
temp_file_uploader.retrieve_from_store!(file_name) end
@temp_file_path = temp_file_uploader.file.path
end end
def file_path def file_path
return @file_path if @file_path strong_memoize(:file_path) do
uploader.file.path
uploader.retrieve_from_store!(file_name) end
@file_path = uploader.file.path
end end
def uploader def uploader
@uploader ||= PersonalFileUploader.new(model, secret: secret) @uploader ||=
begin
uploader = PersonalFileUploader.new(to_model, secret: secret)
# Enforcing a REMOTE object storage given FileUploader#retrieve_from_store! won't do it
# (there's no upload at the target yet).
if uploader.class.object_store_enabled?
uploader.object_store = ::ObjectStorage::Store::REMOTE
end
uploader
end
end end
def temp_file_uploader def temp_file_uploader
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret) @temp_file_uploader ||= PersonalFileUploader.new(from_model, secret: secret)
end end
def revert def revert
Rails.logger.warn("Markdown not updated, file move reverted for #{model}") Rails.logger.warn("Markdown not updated, file move reverted for #{to_model}")
FileUtils.move(file_path, temp_file_path) if temp_file_uploader.file_storage?
FileUtils.move(file_path, temp_file_path)
end
end end
end end

View file

@ -12,7 +12,7 @@
# end # end
# #
class ColorValidator < ActiveModel::EachValidator class ColorValidator < ActiveModel::EachValidator
PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless value =~ PATTERN unless value =~ PATTERN

View file

@ -1,9 +1,10 @@
- header_title _("Snippets"), snippets_path - header_title _("Snippets"), snippets_path
- snippets_upload_path = snippets_upload_path(@snippet, current_user)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
- if @snippet && current_user - if snippets_upload_path
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
:javascript :javascript
window.uploads_path = "#{upload_path('personal_snippet', id: @snippet.id)}"; window.uploads_path = "#{snippets_upload_path}";
= render template: "layouts/application" = render template: "layouts/application"

View file

@ -2,7 +2,7 @@
- issue_votes = @issuable_meta_data[issuable.id] - issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') - issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count(current_user)
- if issuable_mr > 0 - if issuable_mr > 0
%li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') } %li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }

View file

@ -0,0 +1,5 @@
---
title: Persist tmp snippet uploads at users
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Fix DoS vulnerability in color validation regex
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Expose merge requests count based on user access
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Fix Denial of Service for comments when rendering issues/MR comments
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Add missing authorizations in GraphQL
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Prevent Billion Laughs attack
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Gate MR head_pipeline behind read_pipeline ability.
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Correctly check permissions when creating snippet notes
merge_request:
author:
type: security

View file

@ -0,0 +1,5 @@
---
title: Prevent the detection of merge request templates by unauthorized users
merge_request:
author:
type: security

View file

@ -182,7 +182,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# #
# Templates # Templates
# #
get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} } get '/templates/:template_type/:key' => 'templates#show',
as: :template,
defaults: { format: 'json' },
constraints: { key: %r{[^/]+}, template_type: %r{issue|merge_request}, format: 'json' }
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do member do

View file

@ -7,7 +7,7 @@ scope path: :uploads do
# show uploads for models, snippets (notes) available for now # show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename', get '-/system/:model/:id/:secret/:filename',
to: 'uploads#show', to: 'uploads#show',
constraints: { model: /personal_snippet/, id: /\d+/, filename: %r{[^/]+} } constraints: { model: /personal_snippet|user/, id: /\d+/, filename: %r{[^/]+} }
# show temporary uploads # show temporary uploads
get '-/system/temp/:secret/:filename', get '-/system/temp/:secret/:filename',
@ -28,7 +28,7 @@ scope path: :uploads do
# create uploads for models, snippets (notes) available for now # create uploads for models, snippets (notes) available for now
post ':model', post ':model',
to: 'uploads#create', to: 'uploads#create',
constraints: { model: /personal_snippet/, id: /\d+/ }, constraints: { model: /personal_snippet|user/, id: /\d+/ },
as: 'upload' as: 'upload'
end end

View file

@ -498,9 +498,9 @@ module API
expose :state, :created_at, :updated_at expose :state, :created_at, :updated_at
# Avoids an N+1 query when metadata is included # Avoids an N+1 query when metadata is included
def issuable_metadata(subject, options, method) def issuable_metadata(subject, options, method, args = nil)
cached_subject = options.dig(:issuable_metadata, subject.id) cached_subject = options.dig(:issuable_metadata, subject.id)
(cached_subject || subject).public_send(method) # rubocop: disable GitlabSecurity/PublicSend (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
end end
end end
@ -564,7 +564,7 @@ module API
end end
expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) } expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count) } expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) } expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) } expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
expose :due_date expose :due_date
@ -757,7 +757,9 @@ module API
merge_request.metrics&.pipeline merge_request.metrics&.pipeline
end end
expose :head_pipeline, using: 'API::Entities::Pipeline' expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
end
expose :diff_refs, using: Entities::DiffRefs expose :diff_refs, using: Entities::DiffRefs

View file

@ -96,7 +96,7 @@ module API
with: Entities::Issue, with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
} }
present issues, options present issues, options
@ -122,7 +122,7 @@ module API
with: Entities::Issue, with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
} }
present issues, options present issues, options
@ -161,7 +161,7 @@ module API
with_labels_details: declared_params[:with_labels_details], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
project: user_project, project: user_project,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
} }
present issues, options present issues, options

View file

@ -72,7 +72,7 @@ module API
if params[:view] == 'simple' if params[:view] == 'simple'
options[:with] = Entities::MergeRequestSimple options[:with] = Entities::MergeRequestSimple
else else
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest') options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
end end
options options

View file

@ -65,7 +65,7 @@ module API
next unless collection next unless collection
targets = collection.map(&:target) targets = collection.map(&:target)
options[type] = { issuable_metadata: issuable_meta_data(targets, type) } options[type] = { issuable_metadata: issuable_meta_data(targets, type, current_user) }
end end
end end
end end

View file

@ -102,7 +102,7 @@ module Banzai
end end
def relative_file_path(uri) def relative_file_path(uri)
path = Addressable::URI.unescape(uri.path) path = Addressable::URI.unescape(uri.path).delete("\0")
request_path = Addressable::URI.unescape(context[:requested_path]) request_path = Addressable::URI.unescape(context[:requested_path])
nested_path = build_relative_path(path, request_path) nested_path = build_relative_path(path, request_path)
file_exists?(nested_path) ? nested_path : path file_exists?(nested_path) ? nested_path : path

View file

@ -23,6 +23,11 @@ module Gitlab
@root = Entry::Root.new(@config) @root = Entry::Root.new(@config)
@root.compose! @root.compose!
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
Gitlab::Sentry.track_exception(e, extra: { user: user.inspect, project: project.inspect })
raise Config::ConfigError, e.message
rescue *rescue_errors => e rescue *rescue_errors => e
raise Config::ConfigError, e.message raise Config::ConfigError, e.message
end end

View file

@ -4,6 +4,13 @@ module Gitlab
module Config module Config
module Loader module Loader
class Yaml class Yaml
DataTooLargeError = Class.new(Loader::FormatError)
include Gitlab::Utils::StrongMemoize
MAX_YAML_SIZE = 1.megabyte
MAX_YAML_DEPTH = 100
def initialize(config) def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true) @config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e rescue Psych::Exception => e
@ -11,16 +18,35 @@ module Gitlab
end end
def valid? def valid?
@config.is_a?(Hash) hash? && !too_big?
end end
def load! def load!
unless valid? raise DataTooLargeError, 'The parsed YAML is too big' if too_big?
raise Loader::FormatError, 'Invalid configuration format' raise Loader::FormatError, 'Invalid configuration format' unless hash?
end
@config.deep_symbolize_keys @config.deep_symbolize_keys
end end
private
def hash?
@config.is_a?(Hash)
end
def too_big?
return false unless Feature.enabled?(:ci_yaml_limit_size, default_enabled: true)
!deep_size.valid?
end
def deep_size
strong_memoize(:deep_size) do
Gitlab::Utils::DeepSize.new(@config,
max_size: MAX_YAML_SIZE,
max_depth: MAX_YAML_DEPTH)
end
end
end end
end end
end end

View file

@ -39,6 +39,8 @@ module Gitlab
type = node_type_for_basic_connection(type) type = node_type_for_basic_connection(type)
end end
type = type.unwrap if type.kind.non_null?
Array.wrap(type.metadata[:authorize]) Array.wrap(type.metadata[:authorize])
end end

View file

@ -2,7 +2,7 @@
module Gitlab module Gitlab
module IssuableMetadata module IssuableMetadata
def issuable_meta_data(issuable_collection, collection_type) def issuable_meta_data(issuable_collection, collection_type, user = nil)
# ActiveRecord uses Object#extend for null relations. # ActiveRecord uses Object#extend for null relations.
if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) && if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
issuable_collection.respond_to?(:limit_value) && issuable_collection.respond_to?(:limit_value) &&
@ -23,7 +23,7 @@ module Gitlab
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type) issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
issuable_merge_requests_count = issuable_merge_requests_count =
if collection_type == 'Issue' if collection_type == 'Issue'
::MergeRequestsClosingIssues.count_for_collection(issuable_ids) ::MergeRequestsClosingIssues.count_for_collection(issuable_ids, user)
else else
[] []
end end

View file

@ -0,0 +1,79 @@
# frozen_string_literal: true
require 'objspace'
module Gitlab
module Utils
class DeepSize
Error = Class.new(StandardError)
TooMuchDataError = Class.new(Error)
DEFAULT_MAX_SIZE = 1.megabyte
DEFAULT_MAX_DEPTH = 100
def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH)
@root = root
@max_size = max_size
@max_depth = max_depth
@size = 0
@depth = 0
evaluate
end
def valid?
!too_big? && !too_deep?
end
private
def evaluate
add_object(@root)
rescue Error
# NOOP
end
def too_big?
@size > @max_size
end
def too_deep?
@depth > @max_depth
end
def add_object(object)
@size += ObjectSpace.memsize_of(object)
raise TooMuchDataError if @size > @max_size
add_array(object) if object.is_a?(Array)
add_hash(object) if object.is_a?(Hash)
end
def add_array(object)
with_nesting do
object.each do |n|
add_object(n)
end
end
end
def add_hash(object)
with_nesting do
object.each do |key, value|
add_object(key)
add_object(value)
end
end
end
def with_nesting
@depth += 1
raise TooMuchDataError if too_deep?
yield
@depth -= 1
end
end
end
end

View file

@ -0,0 +1,61 @@
# frozen_string_literal: true
require_relative '../../spec_helpers'
module RuboCop
module Cop
module Graphql
class AuthorizeTypes < RuboCop::Cop::Cop
include SpecHelpers
MSG = 'Add an `authorize :ability` call to the type: '\
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
TYPES_DIR = 'app/graphql/types'
# We want to exclude our own basetypes and scalars
WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
QueryType GraphQL::Schema BaseUnion].freeze
def_node_search :authorize?, <<~PATTERN
(send nil? :authorize ...)
PATTERN
def on_class(node)
return unless in_type?(node)
return if whitelisted?(class_constant(node))
return if whitelisted?(superclass_constant(node))
add_offense(node, location: :expression) unless authorize?(node)
end
private
def in_type?(node)
return if in_spec?(node)
path = node.location.expression.source_buffer.name
path.include?(TYPES_DIR)
end
def whitelisted?(class_node)
return false unless class_node&.const_name
WHITELISTED_TYPES.any? { |whitelisted| class_node.const_name.include?(whitelisted) }
end
def class_constant(node)
node.descendants.first
end
def superclass_constant(class_node)
# First one is the class name itself, second is it's superclass
_class_constant, *others = class_node.descendants
others.find { |node| node.const_type? && node&.const_name != 'Types' }
end
end
end
end
end

View file

@ -43,3 +43,4 @@ require_relative 'cop/code_reuse/serializer'
require_relative 'cop/code_reuse/active_record' require_relative 'cop/code_reuse/active_record'
require_relative 'cop/group_public_or_visible_to_user' require_relative 'cop/group_public_or_visible_to_user'
require_relative 'cop/inject_enterprise_edition_module' require_relative 'cop/inject_enterprise_edition_module'
require_relative 'cop/graphql/authorize_types'

View file

@ -252,7 +252,7 @@ describe Projects::NotesController do
before do before do
service_params = ActionController::Parameters.new({ service_params = ActionController::Parameters.new({
note: 'some note', note: 'some note',
noteable_id: merge_request.id.to_s, noteable_id: merge_request.id,
noteable_type: 'MergeRequest', noteable_type: 'MergeRequest',
commit_id: nil, commit_id: nil,
merge_request_diff_head_sha: 'sha' merge_request_diff_head_sha: 'sha'

View file

@ -3,49 +3,101 @@
require 'spec_helper' require 'spec_helper'
describe Projects::TemplatesController do describe Projects::TemplatesController do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository, :private) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:file_path_1) { '.gitlab/issue_templates/issue_template.md' }
let(:file_path_1) { '.gitlab/issue_templates/bug.md' } let(:file_path_2) { '.gitlab/merge_request_templates/merge_request_template.md' }
let(:body) { JSON.parse(response.body) } let(:body) { JSON.parse(response.body) }
let!(:file_1) { project.repository.create_file(user, file_path_1, 'issue content', message: 'message', branch_name: 'master') }
before do let!(:file_2) { project.repository.create_file(user, file_path_2, 'merge request content', message: 'message', branch_name: 'master') }
project.add_developer(user)
sign_in(user)
end
before do
project.add_user(user, Gitlab::Access::MAINTAINER)
project.repository.create_file(user, file_path_1, 'something valid',
message: 'test 3', branch_name: 'master')
end
describe '#show' do describe '#show' do
it 'renders template name and content as json' do shared_examples 'renders issue templates as json' do
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json) it do
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(body["name"]).to eq("bug") expect(body['name']).to eq('issue_template')
expect(body["content"]).to eq("something valid") expect(body['content']).to eq('issue content')
end
end end
it 'renders 404 when unauthorized' do shared_examples 'renders merge request templates as json' do
sign_in(user2) it do
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json) get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
expect(response.status).to eq(404) expect(response.status).to eq(200)
expect(body['name']).to eq('merge_request_template')
expect(body['content']).to eq('merge request content')
end
end end
it 'renders 404 when template type is not found' do shared_examples 'renders 404 when requesting an issue template' do
sign_in(user) it do
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json) get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
expect(response.status).to eq(404) expect(response.status).to eq(404)
end
end end
it 'renders 404 without errors' do shared_examples 'renders 404 when requesting a merge request template' do
sign_in(user) it do
expect { get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json) }.not_to raise_error get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
expect(response.status).to eq(404)
end
end
shared_examples 'renders 404 when params are invalid' do
it 'does not route when the template type is invalid' do
expect do
get(:show, params: { namespace_id: project.namespace, template_type: 'invalid_type', key: 'issue_template', project_id: project }, format: :json)
end.to raise_error(ActionController::UrlGenerationError)
end
it 'renders 404 when the format type is invalid' do
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :html)
expect(response.status).to eq(404)
end
it 'renders 404 when the key is unknown' do
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'unknown_template', project_id: project }, format: :json)
expect(response.status).to eq(404)
end
end
context 'when the user is not a member of the project' do
before do
sign_in(user)
end
include_examples 'renders 404 when requesting an issue template'
include_examples 'renders 404 when requesting a merge request template'
include_examples 'renders 404 when params are invalid'
end
context 'when user is a member of the project' do
before do
project.add_developer(user)
sign_in(user)
end
include_examples 'renders issue templates as json'
include_examples 'renders merge request templates as json'
include_examples 'renders 404 when params are invalid'
end
context 'when user is a guest of the project' do
before do
project.add_guest(user)
sign_in(user)
end
include_examples 'renders issue templates as json'
include_examples 'renders 404 when requesting a merge request template'
include_examples 'renders 404 when params are invalid'
end end
end end
end end

View file

@ -119,6 +119,119 @@ describe Snippets::NotesController do
end end
end end
describe 'POST create' do
context 'when a snippet is public' do
let(:request_params) do
{
note: attributes_for(:note_on_personal_snippet, noteable: public_snippet),
snippet_id: public_snippet.id
}
end
before do
sign_in user
end
it 'returns status 302' do
post :create, params: request_params
expect(response).to have_gitlab_http_status(302)
end
it 'creates the note' do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
end
context 'when a snippet is internal' do
let(:request_params) do
{
note: attributes_for(:note_on_personal_snippet, noteable: internal_snippet),
snippet_id: internal_snippet.id
}
end
before do
sign_in user
end
it 'returns status 302' do
post :create, params: request_params
expect(response).to have_gitlab_http_status(302)
end
it 'creates the note' do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
end
context 'when a snippet is private' do
let(:request_params) do
{
note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
snippet_id: private_snippet.id
}
end
before do
sign_in user
end
context 'when user is not the author' do
before do
sign_in(user)
end
it 'returns status 404' do
post :create, params: request_params
expect(response).to have_gitlab_http_status(404)
end
it 'does not create the note' do
expect { post :create, params: request_params }.not_to change { Note.count }
end
context 'when user sends a snippet_id for a public snippet' do
let(:request_params) do
{
note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
snippet_id: public_snippet.id
}
end
it 'returns status 302' do
post :create, params: request_params
expect(response).to have_gitlab_http_status(302)
end
it 'creates the note on the public snippet' do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
expect(Note.last.noteable).to eq public_snippet
end
end
end
context 'when user is the author' do
before do
sign_in(private_snippet.author)
end
it 'returns status 302' do
post :create, params: request_params
expect(response).to have_gitlab_http_status(302)
end
it 'creates the note' do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
end
end
end
describe 'DELETE destroy' do describe 'DELETE destroy' do
let(:request_params) do let(:request_params) do
{ {

View file

@ -209,8 +209,8 @@ describe SnippetsController do
context 'when the snippet description contains a file' do context 'when the snippet description contains a file' do
include FileMoverHelpers include FileMoverHelpers
let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" }
let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" }
let(:description) do let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\ "Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})" "text: [text.txt](/uploads#{text_file})"

View file

@ -24,121 +24,160 @@ describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
describe 'POST create' do describe 'POST create' do
let(:model) { 'personal_snippet' }
let(:snippet) { create(:personal_snippet, :public) }
let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
context 'when a user does not have permissions to upload a file' do context 'snippet uploads' do
it "returns 401 when the user is not logged in" do let(:model) { 'personal_snippet' }
post :create, params: { model: model, id: snippet.id }, format: :json let(:snippet) { create(:personal_snippet, :public) }
context 'when a user does not have permissions to upload a file' do
it "returns 401 when the user is not logged in" do
post :create, params: { model: model, id: snippet.id }, format: :json
expect(response).to have_gitlab_http_status(401)
end
it "returns 404 when user can't comment on a snippet" do
private_snippet = create(:personal_snippet, :private)
sign_in(user)
post :create, params: { model: model, id: private_snippet.id }, format: :json
expect(response).to have_gitlab_http_status(404)
end
end
context 'when a user is logged in' do
before do
sign_in(user)
end
it "returns an error without file" do
post :create, params: { model: model, id: snippet.id }, format: :json
expect(response).to have_gitlab_http_status(422)
end
it "returns an error with invalid model" do
expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
.to raise_error(ActionController::UrlGenerationError)
end
it "returns 404 status when object not found" do
post :create, params: { model: model, id: 9999 }, format: :json
expect(response).to have_gitlab_http_status(404)
end
context 'with valid image' do
before do
post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
context 'with valid non-image file' do
before do
post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
end
end
context 'user uploads' do
let(:model) { 'user' }
it 'returns 401 when the user has no access' do
post :create, params: { model: 'user', id: user.id }, format: :json
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
it "returns 404 when user can't comment on a snippet" do context 'when user is logged in' do
private_snippet = create(:personal_snippet, :private)
sign_in(user)
post :create, params: { model: model, id: private_snippet.id }, format: :json
expect(response).to have_gitlab_http_status(404)
end
end
context 'when a user is logged in' do
before do
sign_in(user)
end
it "returns an error without file" do
post :create, params: { model: model, id: snippet.id }, format: :json
expect(response).to have_gitlab_http_status(422)
end
it "returns an error with invalid model" do
expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
.to raise_error(ActionController::UrlGenerationError)
end
it "returns 404 status when object not found" do
post :create, params: { model: model, id: 9999 }, format: :json
expect(response).to have_gitlab_http_status(404)
end
context 'with valid image' do
before do before do
post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json sign_in(user)
end end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
context 'with valid non-image file' do
before do
post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
context 'temporal with valid image' do
subject do subject do
post :create, params: { model: 'personal_snippet', file: jpg }, format: :json post :create, params: { model: model, id: user.id, file: jpg }, format: :json
end end
it 'returns a content with original filename, new link, and correct type.' do it 'returns a content with original filename, new link, and correct type.' do
subject subject
expect(response.body).to match '\"alt\":\"rails_sample\"' expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads/-/system/temp" expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
end end
it 'does not create an Upload record' do it 'creates a corresponding Upload record' do
expect { subject }.not_to change { Upload.count } expect { subject }.to change { Upload.count }
end
end
context 'temporal with valid non-image file' do upload = Upload.last
subject do
post :create, params: { model: 'personal_snippet', file: txt }, format: :json aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq user
end
end end
it 'returns a content with original filename, new link, and correct type.' do context 'with valid non-image file' do
subject subject do
post :create, params: { model: model, id: user.id, file: txt }, format: :json
end
expect(response.body).to match '\"alt\":\"doc_sample.txt\"' it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match "\"url\":\"/uploads/-/system/temp" subject
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
end
it 'creates a corresponding Upload record' do
expect { subject }.to change { Upload.count }
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq user
end
end
end end
it 'does not create an Upload record' do it 'returns 404 when given user is not the logged in one' do
expect { subject }.not_to change { Upload.count } another_user = create(:user)
post :create, params: { model: model, id: another_user.id, file: txt }, format: :json
expect(response).to have_gitlab_http_status(404)
end end
end end
end end

View file

@ -41,7 +41,7 @@ describe 'User creates snippet', :js do
expect(page).to have_content('My Snippet') expect(page).to have_content('My Snippet')
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z}) expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
reqs = inspect_requests { visit(link) } reqs = inspect_requests { visit(link) }
expect(reqs.first.status_code).to eq(200) expect(reqs.first.status_code).to eq(200)

View file

@ -7,4 +7,6 @@ describe GitlabSchema.types['Label'] do
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
end end
it { is_expected.to require_graphql_authorizations(:read_label) }
end end

View file

@ -2,4 +2,5 @@ require 'spec_helper'
describe GitlabSchema.types['Metadata'] do describe GitlabSchema.types['Metadata'] do
it { expect(described_class.graphql_name).to eq('Metadata') } it { expect(described_class.graphql_name).to eq('Metadata') }
it { is_expected.to require_graphql_authorizations(:read_instance_metadata) }
end end

View file

@ -13,4 +13,6 @@ describe GitlabSchema.types['Namespace'] do
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
end end
it { is_expected.to require_graphql_authorizations(:read_namespace) }
end end

View file

@ -34,9 +34,5 @@ describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::MetadataType) is_expected.to have_graphql_type(Types::MetadataType)
is_expected.to have_graphql_resolver(Resolvers::MetadataResolver) is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
end end
it 'authorizes with read_instance_metadata' do
is_expected.to require_graphql_authorizations(:read_instance_metadata)
end
end end
end end

View file

@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error expect { filter(act) }.not_to raise_error
end end
it 'does not explode with an escaped null byte' do
act = link("/%00")
expect { filter(act) }.not_to raise_error
end
it 'does not raise an exception with a space in the path' do it 'does not raise an exception with a space in the path' do
act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)") act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)")
expect { filter(act) }.not_to raise_error expect { filter(act) }.not_to raise_error

View file

@ -90,6 +90,27 @@ describe Gitlab::Ci::Config do
end end
end end
context 'when yml is too big' do
let(:yml) do
<<~YAML
--- &1
- hi
- *1
YAML
end
describe '.new' do
it 'raises error' do
expect(Gitlab::Sentry).to receive(:track_exception)
expect { config }.to raise_error(
described_class::ConfigError,
/The parsed YAML is too big/
)
end
end
end
context 'when config logic is incorrect' do context 'when config logic is incorrect' do
let(:yml) { 'before_script: "ls"' } let(:yml) { 'before_script: "ls"' }

View file

@ -8,7 +8,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do describe '#valid?' do
it 'returns true' do it 'returns true' do
expect(loader.valid?).to be true expect(loader).to be_valid
end end
end end
@ -24,7 +24,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do describe '#valid?' do
it 'returns false' do it 'returns false' do
expect(loader.valid?).to be false expect(loader).not_to be_valid
end end
end end
@ -43,7 +43,10 @@ describe Gitlab::Config::Loader::Yaml do
describe '#initialize' do describe '#initialize' do
it 'raises FormatError' do it 'raises FormatError' do
expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias') expect { loader }.to raise_error(
Gitlab::Config::Loader::FormatError,
'Unknown alias: bad_alias'
)
end end
end end
end end
@ -53,7 +56,68 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do describe '#valid?' do
it 'returns false' do it 'returns false' do
expect(loader.valid?).to be false expect(loader).not_to be_valid
end
end
end
# Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
context 'when yaml size is too large' do
let(:yml) do
<<~YAML
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
YAML
end
describe '#valid?' do
it 'returns false' do
expect(loader).not_to be_valid
end
it 'returns true if "ci_yaml_limit_size" feature flag is disabled' do
stub_feature_flags(ci_yaml_limit_size: false)
expect(loader).to be_valid
end
end
describe '#load!' do
it 'raises FormatError' do
expect { loader.load! }.to raise_error(
Gitlab::Config::Loader::FormatError,
'The parsed YAML is too big'
)
end
end
end
# Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
context 'when yaml has cyclic data structure' do
let(:yml) do
<<~YAML
--- &1
- hi
- *1
YAML
end
describe '#valid?' do
it 'returns false' do
expect(loader.valid?).to be(false)
end
end
describe '#load!' do
it 'raises FormatError' do
expect { loader.load! }.to raise_error(Gitlab::Config::Loader::FormatError, 'The parsed YAML is too big')
end end
end end
end end

View file

@ -7,35 +7,39 @@ require 'spec_helper'
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
def type(type_authorizations = []) def type(type_authorizations = [])
Class.new(Types::BaseObject) do Class.new(Types::BaseObject) do
graphql_name "TestType" graphql_name 'TestType'
authorize type_authorizations authorize type_authorizations
end end
end end
def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value") def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options)
Class.new(Types::BaseObject) do Class.new(Types::BaseObject) do
graphql_name "TestTypeWithField" graphql_name 'TestTypeWithField'
field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value} options.reverse_merge!(null: true)
field :test_field, field_type,
authorize: field_authorizations,
resolve: -> (_, _, _) { resolved_value },
**options
end end
end end
let(:current_user) { double(:current_user) } let(:current_user) { double(:current_user) }
subject(:service) { described_class.new(field) } subject(:service) { described_class.new(field) }
describe "#authorized_resolve" do describe '#authorized_resolve' do
let(:presented_object) { double("presented object") } let(:presented_object) { double('presented object') }
let(:presented_type) { double("parent type", object: presented_object) } let(:presented_type) { double('parent type', object: presented_object) }
subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) } subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
context "scalar types" do context 'scalar types' do
shared_examples "checking permissions on the presented object" do shared_examples 'checking permissions on the presented object' do
it "checks the abilities on the object being presented and returns the value" do it 'checks the abilities on the object being presented and returns the value' do
expected_permissions.each do |permission| expected_permissions.each do |permission|
spy_ability_check_for(permission, presented_object, passed: true) spy_ability_check_for(permission, presented_object, passed: true)
end end
expect(resolved).to eq("Resolved value") expect(resolved).to eq('Resolved value')
end end
it "returns nil if the value wasn't authorized" do it "returns nil if the value wasn't authorized" do
@ -45,61 +49,71 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end end
end end
context "when the field is a built-in scalar type" do context 'when the field is a built-in scalar type' do
let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql } let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] } let(:expected_permissions) { [:read_field] }
it_behaves_like "checking permissions on the presented object" it_behaves_like 'checking permissions on the presented object'
end end
context "when the field is a list of scalar types" do context 'when the field is a list of scalar types' do
let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql } let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] } let(:expected_permissions) { [:read_field] }
it_behaves_like "checking permissions on the presented object" it_behaves_like 'checking permissions on the presented object'
end end
context "when the field is sub-classed scalar type" do context 'when the field is sub-classed scalar type' do
let(:field) { type_with_field(Types::TimeType, :read_field).fields["testField"].to_graphql } let(:field) { type_with_field(Types::TimeType, :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] } let(:expected_permissions) { [:read_field] }
it_behaves_like "checking permissions on the presented object" it_behaves_like 'checking permissions on the presented object'
end end
context "when the field is a list of sub-classed scalar types" do context 'when the field is a list of sub-classed scalar types' do
let(:field) { type_with_field([Types::TimeType], :read_field).fields["testField"].to_graphql } let(:field) { type_with_field([Types::TimeType], :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] } let(:expected_permissions) { [:read_field] }
it_behaves_like "checking permissions on the presented object" it_behaves_like 'checking permissions on the presented object'
end end
end end
context "when the field is a specific type" do context 'when the field is a specific type' do
let(:custom_type) { type(:read_type) } let(:custom_type) { type(:read_type) }
let(:object_in_field) { double("presented in field") } let(:object_in_field) { double('presented in field') }
let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql } let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields['testField'].to_graphql }
it "checks both field & type permissions" do it 'checks both field & type permissions' do
spy_ability_check_for(:read_field, object_in_field, passed: true) spy_ability_check_for(:read_field, object_in_field, passed: true)
spy_ability_check_for(:read_type, object_in_field, passed: true) spy_ability_check_for(:read_type, object_in_field, passed: true)
expect(resolved).to eq(object_in_field) expect(resolved).to eq(object_in_field)
end end
it "returns nil if viewing was not allowed" do it 'returns nil if viewing was not allowed' do
spy_ability_check_for(:read_field, object_in_field, passed: false) spy_ability_check_for(:read_field, object_in_field, passed: false)
spy_ability_check_for(:read_type, object_in_field, passed: true) spy_ability_check_for(:read_type, object_in_field, passed: true)
expect(resolved).to be_nil expect(resolved).to be_nil
end end
context "when the field is a list" do context 'when the field is not nullable' do
let(:object_1) { double("presented in field 1") } let(:field) { type_with_field(custom_type, [], object_in_field, null: false).fields['testField'].to_graphql }
let(:object_2) { double("presented in field 2") }
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
it "checks all permissions" do it 'returns nil when viewing is not allowed' do
spy_ability_check_for(:read_type, object_in_field, passed: false)
expect(resolved).to be_nil
end
end
context 'when the field is a list' do
let(:object_1) { double('presented in field 1') }
let(:object_2) { double('presented in field 2') }
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
let(:field) { type_with_field([custom_type], :read_field, presented_types).fields['testField'].to_graphql }
it 'checks all permissions' do
allow(Ability).to receive(:allowed?) { true } allow(Ability).to receive(:allowed?) { true }
spy_ability_check_for(:read_field, object_1, passed: true) spy_ability_check_for(:read_field, object_1, passed: true)
@ -110,7 +124,7 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
expect(resolved).to eq(presented_types) expect(resolved).to eq(presented_types)
end end
it "filters out objects that the user cannot see" do it 'filters out objects that the user cannot see' do
allow(Ability).to receive(:allowed?) { true } allow(Ability).to receive(:allowed?) { true }
spy_ability_check_for(:read_type, object_1, passed: false) spy_ability_check_for(:read_type, object_1, passed: false)

View file

@ -7,11 +7,11 @@ describe Gitlab::IssuableMetadata do
subject { Class.new { include Gitlab::IssuableMetadata }.new } subject { Class.new { include Gitlab::IssuableMetadata }.new }
it 'returns an empty Hash if an empty collection is provided' do it 'returns an empty Hash if an empty collection is provided' do
expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({}) expect(subject.issuable_meta_data(Issue.none, 'Issue', user)).to eq({})
end end
it 'raises an error when given a collection with no limit' do it 'raises an error when given a collection with no limit' do
expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/) expect { subject.issuable_meta_data(Issue.all, 'Issue', user) }.to raise_error(/must have a limit/)
end end
context 'issues' do context 'issues' do
@ -23,7 +23,7 @@ describe Gitlab::IssuableMetadata do
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) } let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do it 'aggregates stats on issues' do
data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue') data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue', user)
expect(data.count).to eq(2) expect(data.count).to eq(2)
expect(data[issue.id].upvotes).to eq(1) expect(data[issue.id].upvotes).to eq(1)
@ -46,7 +46,7 @@ describe Gitlab::IssuableMetadata do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
it 'aggregates stats on merge requests' do it 'aggregates stats on merge requests' do
data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest') data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest', user)
expect(data.count).to eq(2) expect(data.count).to eq(2)
expect(data[merge_request.id].upvotes).to eq(1) expect(data[merge_request.id].upvotes).to eq(1)

View file

@ -0,0 +1,43 @@
require 'spec_helper'
describe Gitlab::Utils::DeepSize do
let(:data) do
{
a: [1, 2, 3],
b: {
c: [4, 5],
d: [
{ e: [[6], [7]] }
]
}
}
end
let(:max_size) { 1.kilobyte }
let(:max_depth) { 10 }
let(:deep_size) { described_class.new(data, max_size: max_size, max_depth: max_depth) }
describe '#evaluate' do
context 'when data within size and depth limits' do
it 'returns true' do
expect(deep_size).to be_valid
end
end
context 'when data not within size limit' do
let(:max_size) { 200.bytes }
it 'returns false' do
expect(deep_size).not_to be_valid
end
end
context 'when data not within depth limit' do
let(:max_depth) { 2 }
it 'returns false' do
expect(deep_size).not_to be_valid
end
end
end
end

View file

@ -58,9 +58,7 @@ describe 'getting projects', :nested_groups do
it 'finds only public projects' do it 'finds only public projects' do
post_graphql(query, current_user: nil) post_graphql(query, current_user: nil)
expect(graphql_data['namespace']['projects']['edges'].size).to eq(1) expect(graphql_data['namespace']).to be_nil
project = graphql_data['namespace']['projects']['edges'][0]['node']
expect(project['id']).to eq(public_project.to_global_id.to_s)
end end
end end
end end

View file

@ -34,4 +34,28 @@ describe 'getting a repository in a project' do
expect(graphql_data['project']).to be(nil) expect(graphql_data['project']).to be(nil)
end end
end end
context 'when the repository is only accessible to members' do
let(:project) do
create(:project, :public, :repository, repository_access_level: ProjectFeature::PRIVATE)
end
it 'returns a repository for the owner' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']['repository']).not_to be_nil
end
it 'returns nil for the repository for other users' do
post_graphql(query, current_user: create(:user))
expect(graphql_data['project']['repository']).to be_nil
end
it 'returns nil for the repository for other users' do
post_graphql(query, current_user: nil)
expect(graphql_data['project']['repository']).to be_nil
end
end
end end

View file

@ -23,7 +23,11 @@ describe API::Issues do
describe 'GET /groups/:id/issues' do describe 'GET /groups/:id/issues' do
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
let!(:private_mrs_project) do
create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
end
let!(:group_closed_issue) do let!(:group_closed_issue) do
create :closed_issue, create :closed_issue,
author: user, author: user,
@ -234,6 +238,30 @@ describe API::Issues do
it_behaves_like 'group issues statistics' it_behaves_like 'group issues statistics'
end end
end end
context "when returns issue merge_requests_count for different access levels" do
let!(:merge_request1) do
create(:merge_request,
:simple,
author: user,
source_project: private_mrs_project,
target_project: private_mrs_project,
description: "closes #{group_issue.to_reference(private_mrs_project)}")
end
let!(:merge_request2) do
create(:merge_request,
:simple,
author: user,
source_project: group_project,
target_project: group_project,
description: "closes #{group_issue.to_reference}")
end
it_behaves_like 'accessible merge requests count' do
let(:api_url) { base_url }
let(:target_issue) { group_issue }
end
end
end end
end end

View file

@ -4,8 +4,9 @@ require 'spec_helper'
describe API::Issues do describe API::Issues do
set(:user) { create(:user) } set(:user) { create(:user) }
set(:project) do set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
create(:project, :public, creator_id: user.id, namespace: user.namespace) set(:private_mrs_project) do
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end end
let(:user2) { create(:user) } let(:user2) { create(:user) }
@ -60,9 +61,28 @@ describe API::Issues do
let(:no_milestone_title) { 'None' } let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' } let(:any_milestone_title) { 'Any' }
let!(:merge_request1) do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "closes #{issue.to_reference}")
end
let!(:merge_request2) do
create(:merge_request,
:simple,
author: user,
source_project: private_mrs_project,
target_project: private_mrs_project,
description: "closes #{issue.to_reference(private_mrs_project)}")
end
before(:all) do before(:all) do
project.add_reporter(user) project.add_reporter(user)
project.add_guest(guest) project.add_guest(guest)
private_mrs_project.add_reporter(user)
private_mrs_project.add_guest(guest)
end end
before do before do
@ -257,6 +277,11 @@ describe API::Issues do
expect_paginated_array_response(issue.id) expect_paginated_array_response(issue.id)
end end
it_behaves_like 'accessible merge requests count' do
let(:api_url) { "/projects/#{project.id}/issues" }
let(:target_issue) { issue }
end
context 'with labeled issues' do context 'with labeled issues' do
let(:label_b) { create(:label, title: 'foo', project: project) } let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) } let(:label_c) { create(:label, title: 'bar', project: project) }
@ -636,34 +661,26 @@ describe API::Issues do
expect(json_response['iid']).to eq(confidential_issue.iid) expect(json_response['iid']).to eq(confidential_issue.iid)
end end
end end
it_behaves_like 'accessible merge requests count' do
let(:api_url) { "/projects/#{project.id}/issues/#{issue.iid}" }
let(:target_issue) { issue }
end
end end
describe 'GET :id/issues/:issue_iid/closed_by' do describe 'GET :id/issues/:issue_iid/closed_by' do
let(:merge_request) do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "closes #{issue.to_reference}")
end
before do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
context 'when unauthenticated' do context 'when unauthenticated' do
it 'return public project issues' do it 'return public project issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by") get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
expect_paginated_array_response(merge_request.id) expect_paginated_array_response(merge_request1.id)
end end
end end
it 'returns merge requests that will close issue on merge' do it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user) get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
expect_paginated_array_response(merge_request.id) expect_paginated_array_response(merge_request1.id)
end end
context 'when no merge requests will close issue' do context 'when no merge requests will close issue' do
@ -721,13 +738,6 @@ describe API::Issues do
end end
it 'returns merge requests that mentioned a issue' do it 'returns merge requests that mentioned a issue' do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: 'Some description')
get_related_merge_requests(project.id, issue.iid, user) get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id) expect_paginated_array_response(related_mr.id)

View file

@ -4,8 +4,9 @@ require 'spec_helper'
describe API::Issues do describe API::Issues do
set(:user) { create(:user) } set(:user) { create(:user) }
set(:project) do set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
create(:project, :public, creator_id: user.id, namespace: user.namespace) set(:private_mrs_project) do
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end end
let(:user2) { create(:user) } let(:user2) { create(:user) }
@ -63,6 +64,8 @@ describe API::Issues do
before(:all) do before(:all) do
project.add_reporter(user) project.add_reporter(user)
project.add_guest(guest) project.add_guest(guest)
private_mrs_project.add_reporter(user)
private_mrs_project.add_guest(guest)
end end
before do before do
@ -725,6 +728,30 @@ describe API::Issues do
end end
end end
end end
context "when returns issue merge_requests_count for different access levels" do
let!(:merge_request1) do
create(:merge_request,
:simple,
author: user,
source_project: private_mrs_project,
target_project: private_mrs_project,
description: "closes #{issue.to_reference(private_mrs_project)}")
end
let!(:merge_request2) do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "closes #{issue.to_reference}")
end
it_behaves_like 'accessible merge requests count' do
let(:api_url) { "/issues" }
let(:target_issue) { issue }
end
end
end end
describe 'DELETE /projects/:id/issues/:issue_iid' do describe 'DELETE /projects/:id/issues/:issue_iid' do

View file

@ -834,6 +834,31 @@ describe API::MergeRequests do
end end
end end
context 'head_pipeline' do
before do
merge_request.update(head_pipeline: create(:ci_pipeline))
merge_request.project.project_feature.update(builds_access_level: 10)
end
context 'when user can read the pipeline' do
it 'exposes pipeline information' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response).to include('head_pipeline')
end
end
context 'when user can not read the pipeline' do
let(:guest) { create(:user) }
it 'does not expose pipeline information' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", guest)
expect(json_response).not_to include('head_pipeline')
end
end
end
it 'returns the commits behind the target branch when include_diverged_commits_count is present' do it 'returns the commits behind the target branch when include_diverged_commits_count is present' do
allow_any_instance_of(merge_request.class).to receive(:diverged_commits_count).and_return(1) allow_any_instance_of(merge_request.class).to receive(:diverged_commits_count).and_return(1)

View file

@ -693,4 +693,24 @@ describe 'project routing' do
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository" it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository"
end end
describe Projects::TemplatesController, 'routing' do
describe '#show' do
def show_with_template_type(template_type)
"/gitlab/gitlabhq/templates/#{template_type}/template_name"
end
it 'routes when :template_type is `merge_request`' do
expect(get(show_with_template_type('merge_request'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'merge_request', key: 'template_name', format: 'json')
end
it 'routes when :template_type is `issue`' do
expect(get(show_with_template_type('issue'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'issue', key: 'template_name', format: 'json')
end
it 'routes to application#route_not_found when :template_type is unknown' do
expect(get(show_with_template_type('invalid'))).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/templates/invalid/template_name')
end
end
end
end end

View file

@ -12,10 +12,19 @@ describe 'Uploads', 'routing' do
) )
end end
it 'does not allow creating uploads for other models' do it 'allows creating uploads for users' do
UploadsController::MODEL_CLASSES.keys.compact.each do |model| expect(post('/uploads/user?id=1')).to route_to(
next if model == 'personal_snippet' controller: 'uploads',
action: 'create',
model: 'user',
id: '1'
)
end
it 'does not allow creating uploads for other models' do
unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user)
unroutable_models.each do |model|
expect(post("/uploads/#{model}?id=1")).not_to be_routable expect(post("/uploads/#{model}?id=1")).not_to be_routable
end end
end end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/graphql/authorize_types'
describe RuboCop::Cop::Graphql::AuthorizeTypes do
include RuboCop::RSpec::ExpectOffense
include CopHelper
subject(:cop) { described_class.new }
context 'when in a type folder' do
before do
allow(cop).to receive(:in_type?).and_return(true)
end
it 'adds an offense when there is no authorize call' do
inspect_source(<<~TYPE)
module Types
class AType < BaseObject
field :a_thing
field :another_thing
end
end
TYPE
expect(cop.offenses.size).to eq 1
end
it 'does not add an offense for classes that have an authorize call' do
expect_no_offenses(<<~TYPE.strip)
module Types
class AType < BaseObject
graphql_name 'ATypeName'
authorize :an_ability, :second_ability
field :a_thing
end
end
TYPE
end
it 'does not add an offense for classes that only have an authorize call' do
expect_no_offenses(<<~TYPE.strip)
module Types
class AType < SuperClassWithFields
authorize :an_ability
end
end
TYPE
end
it 'does not add an offense for base types' do
expect_no_offenses(<<~TYPE)
module Types
class AType < BaseEnum
field :a_thing
end
end
TYPE
end
end
end

View file

@ -0,0 +1,37 @@
def get_issue
json_response.is_a?(Array) ? json_response.detect {|issue| issue['id'] == target_issue.id} : json_response
end
shared_examples 'accessible merge requests count' do
it 'returns anonymous accessible merge requests count' do
get api(api_url), params: { scope: 'all' }
issue = get_issue
expect(issue).not_to be_nil
expect(issue['merge_requests_count']).to eq(1)
end
it 'returns guest accessible merge requests count' do
get api(api_url, guest), params: { scope: 'all' }
issue = get_issue
expect(issue).not_to be_nil
expect(issue['merge_requests_count']).to eq(1)
end
it 'returns reporter accessible merge requests count' do
get api(api_url, user), params: { scope: 'all' }
issue = get_issue
expect(issue).not_to be_nil
expect(issue['merge_requests_count']).to eq(2)
end
it 'returns admin accessible merge requests count' do
get api(api_url, admin), params: { scope: 'all' }
issue = get_issue
expect(issue).not_to be_nil
expect(issue['merge_requests_count']).to eq(2)
end
end

View file

@ -3,79 +3,140 @@ require 'spec_helper'
describe FileMover do describe FileMover do
include FileMoverHelpers include FileMoverHelpers
let(:user) { create(:user) }
let(:filename) { 'banana_sample.gif' } let(:filename) { 'banana_sample.gif' }
let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } let(:secret) { 'secret55' }
let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", secret, filename) }
let(:temp_description) do let(:temp_description) do
"test ![banana_sample](/#{temp_file_path}) "\ "test ![banana_sample](/#{temp_file_path}) "\
"same ![banana_sample](/#{temp_file_path}) " "same ![banana_sample](/#{temp_file_path}) "
end end
let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) } let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, secret, filename) }
let(:snippet) { create(:personal_snippet, description: temp_description) } let(:snippet) { create(:personal_snippet, description: temp_description) }
subject { described_class.new(temp_file_path, snippet).execute } let(:tmp_uploader) do
PersonalFileUploader.new(user, secret: secret)
end
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
subject { described_class.new(temp_file_path, from_model: user, to_model: snippet).execute }
describe '#execute' do describe '#execute' do
let(:tmp_upload) { tmp_uploader.upload }
before do before do
expect(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path))) tmp_uploader.store!(file)
expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
stub_file_mover(temp_file_path)
end end
context 'when move and field update successful' do context 'local storage' do
it 'updates the description correctly' do
subject
expect(snippet.reload.description)
.to eq(
"test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "
)
end
it 'creates a new update record' do
expect { subject }.to change { Upload.count }.by(1)
end
it 'schedules a background migration' do
expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
subject
end
end
context 'when update_markdown fails' do
before do before do
expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path)) allow(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path)))
allow(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
stub_file_mover(temp_file_path)
end end
subject { described_class.new(file_path, snippet, :non_existing_field).execute } context 'when move and field update successful' do
it 'updates the description correctly' do
subject
it 'does not update the description' do expect(snippet.reload.description)
subject .to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
end
expect(snippet.reload.description) it 'updates existing upload record' do
.to eq( expect { subject }
"test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "\ .to change { tmp_upload.reload.attributes.values_at('model_id', 'model_type') }
"same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) " .from([user.id, 'User']).to([snippet.id, 'Snippet'])
) end
it 'schedules a background migration' do
expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
subject
end
end end
it 'does not create a new update record' do context 'when update_markdown fails' do
expect { subject }.not_to change { Upload.count } before do
expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path))
end
subject { described_class.new(file_path, :non_existing_field, from_model: user, to_model: snippet).execute }
it 'does not update the description' do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
end
it 'does not change the upload record' do
expect { subject }
.not_to change { tmp_upload.reload.attributes.values_at('model_id', 'model_type') }
end
end
end
context 'when tmp uploader is not local storage' do
before do
allow(PersonalFileUploader).to receive(:object_store_enabled?) { true }
tmp_uploader.object_store = ObjectStorage::Store::REMOTE
allow_any_instance_of(PersonalFileUploader).to receive(:file_storage?) { false }
end
after do
FileUtils.rm_f(File.join('personal_snippet', snippet.id.to_s, secret, filename))
end
context 'when move and field update successful' do
it 'updates the description correctly' do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
end
it 'creates new target upload record an delete the old upload' do
expect { subject }
.to change { Upload.last.attributes.values_at('model_id', 'model_type') }
.from([user.id, 'User']).to([snippet.id, 'Snippet'])
expect(Upload.count).to eq(1)
end
end
context 'when update_markdown fails' do
subject { described_class.new(file_path, :non_existing_field, from_model: user, to_model: snippet).execute }
it 'does not update the description' do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
end
it 'does not change the upload record' do
expect { subject }
.to change { Upload.last.attributes.values_at('model_id', 'model_type') }.from([user.id, 'User'])
end
end end
end end
end end
context 'security' do context 'security' do
context 'when relative path is involved' do context 'when relative path is involved' do
let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') } let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", '..', 'another_subdir_of_temp') }
it 'does not trigger move if path is outside designated directory' do it 'does not trigger move if path is outside designated directory' do
stub_file_mover('uploads/-/system/another_subdir_of_temp') stub_file_mover("uploads/-/system/user/#{user.id}/another_subdir_of_temp")
expect(FileUtils).not_to receive(:move) expect(FileUtils).not_to receive(:move)
subject subject

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'spec_helper'
describe ColorValidator do
using RSpec::Parameterized::TableSyntax
subject do
Class.new do
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :color
validates :color, color: true
end.new
end
where(:color, :is_valid) do
'#000abc' | true
'#aaa' | true
'#BBB' | true
'#cCc' | true
'#ffff' | false
'#000111222' | false
'invalid' | false
'000' | false
end
with_them do
it 'only accepts valid colors' do
subject.color = color
expect(subject.valid?).to eq(is_valid)
end
end
it 'fails fast for long invalid string' do
subject.color = '#' + ('0' * 50_000) + 'xxx'
expect do
Timeout.timeout(5.seconds) { subject.valid? }
end.not_to raise_error
end
end