Commit f66169b3 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'security-59581-related-merge-requests-count' into 'master'

Expose merge requests count based on user access

See merge request gitlab/gitlabhq!3157
parents f4cc3db3 09163e42
...@@ -42,7 +42,7 @@ module IssuableCollections ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -298,7 +298,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -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
......
...@@ -280,7 +280,7 @@ module IssuablesHelper ...@@ -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
......
...@@ -29,7 +29,11 @@ module Issuable ...@@ -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 ...@@ -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 ...@@ -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
...@@ -2,7 +2,7 @@ ...@@ -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') }
......
---
title: Expose merge requests count based on user access
merge_request:
author:
type: security
...@@ -498,9 +498,9 @@ module API ...@@ -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 ...@@ -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
......
...@@ -96,7 +96,7 @@ module API ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -2,7 +2,7 @@ ...@@ -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 ...@@ -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
......
...@@ -7,11 +7,11 @@ describe Gitlab::IssuableMetadata do ...@@ -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 ...@@ -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 ...@@ -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)
......
...@@ -23,7 +23,11 @@ describe API::Issues do ...@@ -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 ...@@ -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' ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
end
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 it_behaves_like 'accessible merge requests count' do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) 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
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 ...@@ -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' ...@@ -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 ...@@ -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 ...@@ -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
......
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment