gitlab-org--gitlab-foss/app/finders/issuable_finder.rb

442 lines
11 KiB
Ruby
Raw Normal View History

# IssuableFinder
#
# Used to filter Issues and MergeRequests collections by set of params
#
# Arguments:
# klass - actual class like Issue or MergeRequest
# current_user - which user use
# params:
# scope: 'created-by-me' or 'assigned-to-me' or 'all'
# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
# sort: string
# non_archived: boolean
2017-02-17 13:28:32 -05:00
# iids: integer[]
#
class IssuableFinder
include CreatedAtFilter
2017-02-21 18:32:18 -05:00
NONE = '0'.freeze
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page state].freeze
2015-03-26 21:56:42 -04:00
attr_accessor :current_user, :params
Merge branch 'jej-use-issuable-finder-instead-of-access-check' into 'security' Replace issue access checks with use of IssuableFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 ## Which fixes are in this MR? :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested ### Issue lookup with access check Using `visible_to_user` likely makes these security issues too. See [Code smells](#code-smells). - [x] :vertical_traffic_light: app/finders/notes_finder.rb:15 [`visible_to_user`] - [x] :traffic_light: app/views/layouts/nav/_project.html.haml:73 [`visible_to_user`] [`.count`] - [x] :white_check_mark: app/services/merge_requests/build_service.rb:84 [`issue.try(:confidential?)`] - [x] :white_check_mark: lib/api/issues.rb:112 [`visible_to_user`] - CHANGELOG: Prevented API returning issues set to 'Only team members' to everyone - [x] :white_check_mark: lib/api/helpers.rb:126 [`can?(current_user, :read_issue, issue)`] Maybe here too? - [x] :white_check_mark: lib/gitlab/search_results.rb:53 [`visible_to_user`] ### Previous discussions - [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#b2ff264eddf9819d7693c14ae213d941494fe2b3_128_126 - [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#7b6375270d22f880bdcb085e47b519b426a5c6c7_87_87 See merge request !2031
2016-11-22 05:25:04 -05:00
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
def execute
2014-02-03 10:02:44 -05:00
items = init_collection
items = by_scope(items)
items = by_created_at(items)
items = by_state(items)
items = by_group(items)
items = by_search(items)
items = by_assignee(items)
items = by_author(items)
items = by_due_date(items)
items = by_non_archived(items)
2017-02-17 13:28:32 -05:00
items = by_iids(items)
2017-02-07 08:15:07 -05:00
items = by_milestone(items)
items = by_label(items)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items = by_project(items)
2015-10-03 01:56:37 -04:00
sort(items)
end
def find(*params)
execute.find(*params)
end
def find_by(*params)
execute.find_by(*params)
end
# We often get counts for each state by running a query per state, and
# counting those results. This is typically slower than running one query
# (even if that query is slower than any of the individual state queries) and
# grouping and counting within that query.
#
def count_by_state
count_params = params.merge(state: nil, sort: nil, for_counting: true)
labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
# Searching by label includes a GROUP BY in the query, but ours will be last
# because it is added last. Searching by multiple labels also includes a row
# per issuable, so we have to count those in Ruby - which is bad, but still
# better than performing multiple queries.
#
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count
end
counts[:all] = counts.values.sum
counts
end
Merge branch 'jej-23867-use-mr-finder-instead-of-access-check' into 'security' Replace MR access checks with use of MergeRequestsFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested - [x] :bomb: app/finders/notes_finder.rb:17 - [x] :warning: app/views/layouts/nav/_project.html.haml:80 [`.count`] - [x] :bomb: app/controllers/concerns/creates_commit.rb:84 - [x] :traffic_light: app/controllers/projects/commits_controller.rb:24 - [x] :traffic_light: app/controllers/projects/compare_controller.rb:56 - [x] :vertical_traffic_light: app/controllers/projects/discussions_controller.rb:29 - [x] :white_check_mark: app/controllers/projects/todos_controller.rb:27 - [x] :vertical_traffic_light: app/models/commit.rb:268 - [x] :white_check_mark: lib/gitlab/search_results.rb:71 - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_267_266 Memoize ` merged_merge_request(current_user)` - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_248_247 Expected side effect for `merged_merge_request!`, consider `skip_authorization: true`. - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_269_269 Scary use of unchecked `merged_merge_request?` See merge request !2033
2016-11-29 08:47:43 -05:00
def find_by!(*params)
execute.find_by!(*params)
end
def state_counter_cache_key
cache_key(state_counter_cache_key_components)
end
def clear_caches!
state_counter_cache_key_components_permutations.each do |components|
Rails.cache.delete(cache_key(components))
end
end
def group
return @group if defined?(@group)
@group =
if params[:group_id].present?
Group.find(params[:group_id])
else
nil
end
end
def project?
params[:project_id].present?
end
def project
return @project if defined?(@project)
project = Project.find(params[:project_id])
project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
@project = project
end
2017-02-07 08:15:07 -05:00
def projects(items = nil)
return @projects = project if project?
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group: group, current_user: current_user).execute
else
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
def search
params[:search].presence
end
def milestones?
params[:milestone_title].present?
end
2015-10-19 05:46:22 -04:00
def filter_by_no_milestone?
milestones? && params[:milestone_title] == Milestone::None.title
end
def milestones
return @milestones if defined?(@milestones)
@milestones =
if milestones?
2017-07-07 11:08:49 -04:00
if project?
group_id = project.group&.id
project_id = project.id
end
group_id = group.id if group
2015-10-14 06:20:48 -04:00
2017-07-07 11:08:49 -04:00
search_params =
{ title: params[:milestone_title], project_ids: project_id, group_ids: group_id }
MilestonesFinder.new(search_params).execute
else
Milestone.none
end
end
def labels?
params[:label_name].present?
end
2015-10-19 05:46:22 -04:00
def filter_by_no_label?
labels? && params[:label_name].include?(Label::None.title)
end
2016-02-19 00:27:41 -05:00
def labels
return @labels if defined?(@labels)
@labels =
if labels? && !filter_by_no_label?
LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
else
Label.none
2016-02-19 00:27:41 -05:00
end
end
def assignee_id?
params[:assignee_id].present? && params[:assignee_id] != NONE
end
def assignee_username?
params[:assignee_username].present? && params[:assignee_username] != NONE
end
def no_assignee?
2017-01-10 20:51:57 -05:00
# Assignee_id takes precedence over assignee_username
params[:assignee_id] == NONE || params[:assignee_username] == NONE
end
def assignee
return @assignee if defined?(@assignee)
@assignee =
if assignee_id?
User.find_by(id: params[:assignee_id])
elsif assignee_username?
User.find_by(username: params[:assignee_username])
else
nil
end
end
def author_id?
params[:author_id].present? && params[:author_id] != NONE
end
def author_username?
params[:author_username].present? && params[:author_username] != NONE
end
def no_author?
2017-01-10 20:51:57 -05:00
# author_id takes precedence over author_username
params[:author_id] == NONE || params[:author_username] == NONE
end
def author
return @author if defined?(@author)
@author =
if author_id?
User.find_by(id: params[:author_id])
elsif author_username?
User.find_by(username: params[:author_username])
else
nil
end
end
private
2014-02-03 10:02:44 -05:00
def init_collection
klass.all
2014-02-03 10:02:44 -05:00
end
def by_scope(items)
2016-03-21 19:09:20 -04:00
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
2016-03-21 19:09:20 -04:00
when 'assigned-to-me'
items.assigned_to(current_user)
else
2016-03-21 19:09:20 -04:00
items
end
end
def by_state(items)
case params[:state].to_s
when 'closed'
items.closed
when 'merged'
items.respond_to?(:merged) ? items.merged : items.closed
when 'opened'
items.opened
else
items
end
end
def by_group(items)
# Selection by group is already covered by `by_project` and `projects`
items
end
def by_project(items)
items =
if project?
2017-02-07 08:15:07 -05:00
items.of_projects(projects(items)).references_project
elsif projects(items)
items.merge(projects(items).reorder(nil)).join_project
else
items.none
end
items
end
def by_search(items)
2017-02-17 13:28:32 -05:00
search ? items.full_search(search) : items
end
2017-02-17 13:28:32 -05:00
def by_iids(items)
params[:iids].present? ? items.where(iid: params[:iids]) : items
end
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end
def by_assignee(items)
if assignee
items = items.where(assignee_id: assignee.id)
elsif no_assignee?
items = items.where(assignee_id: nil)
elsif assignee_id? || assignee_username? # assignee not found
items = items.none
end
items
end
def by_author(items)
if author
items = items.where(author_id: author.id)
elsif no_author?
items = items.where(author_id: nil)
elsif author_id? || author_username? # author not found
items = items.none
end
items
end
2016-03-13 08:19:27 -04:00
def filter_by_upcoming_milestone?
params[:milestone_title] == Milestone::Upcoming.name
2016-03-11 12:46:14 -05:00
end
def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name
end
def by_milestone(items)
if milestones?
2015-10-19 05:46:22 -04:00
if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil])
2016-03-13 08:19:27 -04:00
elsif filter_by_upcoming_milestone?
2017-02-07 08:15:07 -05:00
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
elsif filter_by_started_milestone?
items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else
items = items.with_milestone(params[:milestone_title])
end
end
items
end
def by_label(items)
if labels?
2015-10-19 05:46:22 -04:00
if filter_by_no_label?
2016-03-06 23:07:19 -05:00
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
2017-02-07 08:15:07 -05:00
items_projects = projects(items)
2017-02-07 08:15:07 -05:00
if items_projects
label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids })
end
end
end
items
end
def by_due_date(items)
if due_date?
if filter_by_no_due_date?
items = items.without_due_date
elsif filter_by_overdue?
items = items.due_before(Date.today)
elsif filter_by_due_this_week?
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
elsif filter_by_due_this_month?
items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
end
end
items
end
def filter_by_no_due_date?
due_date? && params[:due_date] == Issue::NoDueDate.name
end
def filter_by_overdue?
due_date? && params[:due_date] == Issue::Overdue.name
end
def filter_by_due_this_week?
due_date? && params[:due_date] == Issue::DueThisWeek.name
end
def filter_by_due_this_month?
due_date? && params[:due_date] == Issue::DueThisMonth.name
end
def due_date?
params[:due_date].present? && klass.column_names.include?('due_date')
end
2016-02-19 00:27:41 -05:00
def label_names
if labels?
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
else
[]
end
2016-02-19 00:27:41 -05:00
end
def by_non_archived(items)
params[:non_archived].present? ? items.non_archived : items
end
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
def state_counter_cache_key_components
opts = params.with_indifferent_access
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
['issuables_count', klass.to_ability_name, opts.sort]
end
def state_counter_cache_key_components_permutations
[state_counter_cache_key_components]
end
def cache_key(components)
Digest::SHA1.hexdigest(components.flatten.join('-'))
end
end