Commit 62aaaa1b authored by Micael Bergeron's avatar Micael Bergeron

Optimize the ES Query for confidentiality check

Previously, we would do separate checks for project membership and
confidentiality checks.

This commit adds an optimization that will limit the confidentiality
check to be within the limited projects, if such a limit exists.
parent 1e5cc59b
---
title: Optimize the Advanced Search query for Issues and Notes.
merge_request: 38095
author:
type: performance
...@@ -171,7 +171,7 @@ module Elastic ...@@ -171,7 +171,7 @@ module Elastic
limit = limit =
{ terms: { "#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE] } } { terms: { "#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE] } }
{ bool: { filter: [condition, limit].compact } } { bool: { filter: [condition, limit] } }
end end
end end
...@@ -226,11 +226,24 @@ module Elastic ...@@ -226,11 +226,24 @@ module Elastic
# When reading cross project is not allowed, only allow searching a # When reading cross project is not allowed, only allow searching a
# a single project, so the `:read_*` ability is only checked once. # a single project, so the `:read_*` ability is only checked once.
unless Ability.allowed?(current_user, :read_cross_project) unless Ability.allowed?(current_user, :read_cross_project)
project_ids = [] if project_ids.size > 1 return [] if project_ids.size > 1
end end
project_ids project_ids
end end
def authorized_project_ids(current_user, options = {})
return [] unless current_user
scoped_project_ids = scoped_project_ids(current_user, options[:project_ids])
authorized_project_ids = current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key.to_set
# if the current search is limited to a subset of projects, we should do
# confidentiality check for these projects.
authorized_project_ids &= scoped_project_ids.to_set unless scoped_project_ids == :any
authorized_project_ids.to_a
end
end end
end end
end end
...@@ -13,29 +13,32 @@ module Elastic ...@@ -13,29 +13,32 @@ module Elastic
options[:features] = 'issues' options[:features] = 'issues'
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user], options[:project_ids]) query_hash = confidentiality_filter(query_hash, options)
search(query_hash, options) search(query_hash, options)
end end
private private
def user_has_access_to_confidential_issues?(authorized_project_ids, project_ids) def confidentiality_filter(query_hash, options)
# is_a?(Array) is needed because we might receive project_ids: :any current_user = options[:current_user]
return false unless authorized_project_ids && project_ids.is_a?(Array) project_ids = options[:project_ids]
(project_ids - authorized_project_ids).empty?
end
def confidentiality_filter(query_hash, current_user, project_ids)
return query_hash if current_user&.can_read_all_resources? return query_hash if current_user&.can_read_all_resources?
authorized_project_ids = current_user&.authorized_projects(Gitlab::Access::REPORTER)&.pluck_primary_key scoped_project_ids = scoped_project_ids(current_user, project_ids)
return query_hash if user_has_access_to_confidential_issues?(authorized_project_ids, project_ids) authorized_project_ids = authorized_project_ids(current_user, options)
# we can shortcut the filter if the user is authorized to see
# all the projects for which this query is scoped on
unless scoped_project_ids == :any || scoped_project_ids.empty?
return query_hash if authorized_project_ids.to_set == scoped_project_ids.to_set
end
filter = filter = { term: { confidential: false } }
if current_user
{ if current_user
filter = {
bool: { bool: {
should: [ should: [
{ term: { confidential: false } }, { term: { confidential: false } },
...@@ -58,9 +61,7 @@ module Elastic ...@@ -58,9 +61,7 @@ module Elastic
] ]
} }
} }
else end
{ term: { confidential: false } }
end
query_hash[:query][:bool][:filter] << filter query_hash[:query][:bool][:filter] << filter
query_hash query_hash
......
...@@ -14,7 +14,7 @@ module Elastic ...@@ -14,7 +14,7 @@ module Elastic
query_hash = basic_query_hash(%w[note], query) query_hash = basic_query_hash(%w[note], query)
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user]) query_hash = confidentiality_filter(query_hash, options)
query_hash[:highlight] = highlight_options(options[:in]) query_hash[:highlight] = highlight_options(options[:in])
...@@ -23,7 +23,9 @@ module Elastic ...@@ -23,7 +23,9 @@ module Elastic
private private
def confidentiality_filter(query_hash, current_user) def confidentiality_filter(query_hash, options)
current_user = options[:current_user]
return query_hash if current_user&.can_read_all_resources? return query_hash if current_user&.can_read_all_resources?
filter = { filter = {
...@@ -43,7 +45,7 @@ module Elastic ...@@ -43,7 +45,7 @@ module Elastic
bool: { bool: {
should: [ should: [
{ bool: { must_not: [{ exists: { field: :confidential } }] } }, { bool: { must_not: [{ exists: { field: :confidential } }] } },
{ term: { "confidential" => false } } { term: { confidential: false } }
] ]
} }
} }
...@@ -61,7 +63,7 @@ module Elastic ...@@ -61,7 +63,7 @@ module Elastic
bool: { bool: {
should: [ should: [
{ term: { "issue.confidential" => true } }, { term: { "issue.confidential" => true } },
{ term: { "confidential" => true } } { term: { confidential: true } }
] ]
} }
}, },
...@@ -70,7 +72,7 @@ module Elastic ...@@ -70,7 +72,7 @@ module Elastic
should: [ should: [
{ term: { "issue.author_id" => current_user.id } }, { term: { "issue.author_id" => current_user.id } },
{ term: { "issue.assignee_id" => current_user.id } }, { term: { "issue.assignee_id" => current_user.id } },
{ terms: { "project_id" => current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key } } { terms: { project_id: authorized_project_ids(current_user, options) } }
] ]
} }
} }
......
...@@ -261,6 +261,7 @@ RSpec.configure do |config| ...@@ -261,6 +261,7 @@ RSpec.configure do |config|
./spec/support/protected_tags ./spec/support/protected_tags
./spec/support/shared_examples/features ./spec/support/shared_examples/features
./spec/support/shared_examples/requests ./spec/support/shared_examples/requests
./spec/support/shared_examples/lib/gitlab
./spec/views ./spec/views
./spec/workers ./spec/workers
) )
......
...@@ -10,10 +10,6 @@ RSpec.shared_examples 'access restricted confidential issues' do ...@@ -10,10 +10,6 @@ RSpec.shared_examples 'access restricted confidential issues' do
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
before do
stub_feature_flags(user_mode_in_session: false)
end
subject(:objects) do subject(:objects) do
described_class.new(user, query, project: project).objects('issues') described_class.new(user, query, project: project).objects('issues')
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