...@@ -41,7 +41,7 @@ module IssuableCollections ...@@ -41,7 +41,7 @@ module IssuableCollections
return if pagination_disabled? return if pagination_disabled?
@issuables = @issuables.page(params[:page]) @issuables = @issuables.page(params[:page])
@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
... ...
......
...@@ -297,7 +297,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -297,7 +297,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
... ...
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
# updated_after: datetime # updated_after: datetime
# updated_before: datetime # updated_before: datetime
# attempt_group_search_optimizations: boolean # attempt_group_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# #
class IssuableFinder class IssuableFinder
prepend FinderWithCrossProjectAccess prepend FinderWithCrossProjectAccess
...@@ -184,7 +185,6 @@ class IssuableFinder ...@@ -184,7 +185,6 @@ class IssuableFinder
@project = project @project = project
end end
# rubocop: disable CodeReuse/ActiveRecord
def projects def projects
return @projects if defined?(@projects) return @projects if defined?(@projects)
...@@ -192,17 +192,25 @@ class IssuableFinder ...@@ -192,17 +192,25 @@ class IssuableFinder
projects = projects =
if current_user && params[:authorized_only].presence && !current_user_related? if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects current_user.authorized_projects(min_access_level)
elsif group elsif group
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } find_group_projects
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
else else
ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder Project.public_or_visible_to_user(current_user, min_access_level)
end end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
end
def find_group_projects
return Project.none unless group
if params[:include_subgroups]
Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
else
group.projects
end.public_or_visible_to_user(current_user, min_access_level)
end end
# rubocop: enable CodeReuse/ActiveRecord
def search def search
params[:search].presence params[:search].presence
...@@ -572,4 +580,8 @@ class IssuableFinder ...@@ -572,4 +580,8 @@ class IssuableFinder
scope = params[:scope] scope = params[:scope]
scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me' scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
end end
def min_access_level
ProjectFeature.required_minimum_access_level(klass)
end
end end
...@@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder ...@@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder
OR (issues.confidential = TRUE OR (issues.confidential = TRUE
AND (issues.author_id = :user_id AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))', OR EXISTS (:authorizations)))',
user_id: current_user.id, user_id: current_user.id,
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) authorizations: current_user.authorizations_for_projects(min_access_level: CONFIDENTIAL_ACCESS_LEVEL, related_project_column: "issues.project_id"))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
... ...
......
...@@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder ...@@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder
collection = by_personal(collection) collection = by_personal(collection)
collection = by_starred(collection) collection = by_starred(collection)
collection = by_trending(collection) collection = by_trending(collection)
collection = by_visibilty_level(collection) collection = by_visibility_level(collection)
collection = by_tags(collection) collection = by_tags(collection)
collection = by_search(collection) collection = by_search(collection)
collection = by_archived(collection) collection = by_archived(collection)
...@@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder ...@@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder
collection collection
end end
# rubocop: disable CodeReuse/ActiveRecord
def collection_with_user def collection_with_user
if owned_projects? if owned_projects?
current_user.owned_projects current_user.owned_projects
elsif min_access_level? elsif min_access_level?
current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level]) current_user.authorized_projects(params[:min_access_level])
else else
if private_only? if private_only?
current_user.authorized_projects current_user.authorized_projects
...@@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder ...@@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder
end end
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user. # Builds a collection for an anonymous user.
def collection_without_user def collection_without_user
...@@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder ...@@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def by_visibilty_level(items) def by_visibility_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
... ...
......
...@@ -277,7 +277,7 @@ module IssuablesHelper ...@@ -277,7 +277,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
... ...
......
...@@ -270,8 +270,8 @@ class Issue < ApplicationRecord ...@@ -270,8 +270,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
private private
... ...
......
...@@ -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
...@@ -461,10 +461,12 @@ class Project < ApplicationRecord ...@@ -461,10 +461,12 @@ class Project < ApplicationRecord
# Returns a collection of projects that is either public or visible to the # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
def self.public_or_visible_to_user(user = nil) def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.admin?
if user if user
where('EXISTS (?) OR projects.visibility_level IN (?)', where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects, user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user)) Gitlab::VisibilityLevel.levels_for_user(user))
else else
public_to_user public_to_user
...@@ -474,30 +476,32 @@ class Project < ApplicationRecord ...@@ -474,30 +476,32 @@ class Project < ApplicationRecord
# project features may be "disabled", "internal", "enabled" or "public". If "internal", # project features may be "disabled", "internal", "enabled" or "public". If "internal",
# they are only available to team members. This scope returns projects where # they are only available to team members. This scope returns projects where
# the feature is either public, enabled, or internal with permission for the user. # the feature is either public, enabled, or internal with permission for the user.
# Note: this scope doesn't enforce that the user has access to the projects, it just checks
# that the user has access to the feature. It's important to use this scope with others
# that checks project authorizations first.
# #
# This method uses an optimised version of `with_feature_access_level` for # This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given # logged in users to more efficiently get private projects with the given
# feature. # feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC] visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin? if user&.admin?
with_feature_enabled(feature) with_feature_enabled(feature)
elsif user elsif user
min_access_level = ProjectFeature.required_minimum_access_level(feature)
column = ProjectFeature.quoted_access_level_column(feature) column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature with_project_feature
.where( .where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
"(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
" OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
{ {
private: Gitlab::VisibilityLevel::PRIVATE, public_visible: visible,
public_visible: ProjectFeature::ENABLED,
private_visible: ProjectFeature::PRIVATE, private_visible: ProjectFeature::PRIVATE,
authorizations: user.authorizations_for_projects(min_access_level: min_access_level) authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
}) })
else else
# This has to be added to include features whose value is nil in the db
visible << nil
with_feature_access_level(feature, visible) with_feature_access_level(feature, visible)
end end
end end
... ...
......
...@@ -761,11 +761,15 @@ class User < ApplicationRecord ...@@ -761,11 +761,15 @@ class User < ApplicationRecord
# Typically used in conjunction with projects table to get projects # Typically used in conjunction with projects table to get projects
# a user has been given access to. # a user has been given access to.
# The param `related_project_column` is the column to compare to the
# project_authorizations. By default is projects.id
# #
# Example use: # Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)` # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
def authorizations_for_projects(min_access_level: nil) def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id') authorizations = project_authorizations
.select(1)
.where("project_authorizations.project_id = #{related_project_column}")
return authorizations unless min_access_level.present? return authorizations unless min_access_level.present?
... ...
......
...@@ -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: Add improvements to global search of issues and merge requests
merge_request: 27817
author:
type: performance
---
title: Expose merge requests count based on user access
merge_request:
author:
type: security
...@@ -493,9 +493,9 @@ module API ...@@ -493,9 +493,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
...@@ -554,7 +554,7 @@ module API ...@@ -554,7 +554,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
... ...
......
...@@ -93,7 +93,7 @@ module API ...@@ -93,7 +93,7 @@ module API
options = { options = {
with: Entities::IssueBasic, with: Entities::IssueBasic,
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
...@@ -120,7 +120,7 @@ module API ...@@ -120,7 +120,7 @@ module API
options = { options = {
with: Entities::IssueBasic, with: Entities::IssueBasic,
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
...@@ -150,7 +150,7 @@ module API ...@@ -150,7 +150,7 @@ module API
with: Entities::IssueBasic, with: Entities::IssueBasic,
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
... ...
......
...@@ -71,7 +71,7 @@ module API ...@@ -71,7 +71,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,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Gitlab module Gitlab
class GroupSearchResults < SearchResults class GroupSearchResults < SearchResults
attr_reader :group
def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20) def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page) super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
...@@ -26,5 +28,9 @@ module Gitlab ...@@ -26,5 +28,9 @@ module Gitlab
.where(id: groups.select('members.user_id')) .where(id: groups.select('members.user_id'))
end end
# rubocop:enable CodeReuse/ActiveRecord # rubocop:enable CodeReuse/ActiveRecord
def issuable_params
super.merge(group_id: group.id)
end
end end
end end