Merge branch 'master' of dev.gitlab.org:gitlab/gitlabhq
This commit is contained in:
commit
49d689fb3c
85 changed files with 1438 additions and 332 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
app/policies/repository_policy.rb
Normal file
5
app/policies/repository_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RepositoryPolicy < BasePolicy
|
||||||
|
delegate { @subject.project }
|
||||||
|
end
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Persist tmp snippet uploads at users
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix DoS vulnerability in color validation regex
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Expose merge requests count based on user access
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix Denial of Service for comments when rendering issues/MR comments
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add missing authorizations in GraphQL
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Prevent Billion Laughs attack
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
5
changelogs/unreleased/security-mr-head-pipeline-leak.yml
Normal file
5
changelogs/unreleased/security-mr-head-pipeline-leak.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Gate MR head_pipeline behind read_pipeline ability.
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Correctly check permissions when creating snippet notes
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Prevent the detection of merge request templates by unauthorized users
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: security
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
79
lib/gitlab/utils/deep_size.rb
Normal file
79
lib/gitlab/utils/deep_size.rb
Normal 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
|
61
rubocop/cop/graphql/authorize_types.rb
Normal file
61
rubocop/cop/graphql/authorize_types.rb
Normal 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
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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})"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"' }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
43
spec/lib/gitlab/utils/deep_size_spec.rb
Normal file
43
spec/lib/gitlab/utils/deep_size_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
66
spec/rubocop/cop/graphql/authorize_types_spec.rb
Normal file
66
spec/rubocop/cop/graphql/authorize_types_spec.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
43
spec/validators/color_validator_spec.rb
Normal file
43
spec/validators/color_validator_spec.rb
Normal 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
|
Loading…
Reference in a new issue