Commit 631b0307 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch...

Merge branch '300351-remove-joins-from-the-elasticsearch-query-for-project-group-scoped-notes-comments-search-3' into 'master'

Add permission for indexed notes & update when they change (2nd attempt)

See merge request gitlab-org/gitlab!54482
parents 76236248 2eb7d628
...@@ -108,6 +108,7 @@ module EE ...@@ -108,6 +108,7 @@ module EE
has_one :security_orchestration_policy_configuration, class_name: 'Security::OrchestrationPolicyConfiguration', foreign_key: :project_id, inverse_of: :project has_one :security_orchestration_policy_configuration, class_name: 'Security::OrchestrationPolicyConfiguration', foreign_key: :project_id, inverse_of: :project
elastic_index_dependant_association :issues, on_change: :visibility_level elastic_index_dependant_association :issues, on_change: :visibility_level
elastic_index_dependant_association :notes, on_change: :visibility_level
scope :with_shared_runners_limit_enabled, -> do scope :with_shared_runners_limit_enabled, -> do
if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost? if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost?
......
...@@ -5,6 +5,12 @@ module EE ...@@ -5,6 +5,12 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
EE_FEATURES = %i(requirements).freeze EE_FEATURES = %i(requirements).freeze
NOTES_PERMISSION_TRACKED_FIELDS = %w(
issues_access_level
repository_access_level
merge_requests_access_level
snippets_access_level
).freeze
prepended do prepended do
set_available_features(EE_FEATURES) set_available_features(EE_FEATURES)
...@@ -14,7 +20,11 @@ module EE ...@@ -14,7 +20,11 @@ module EE
if project.maintaining_elasticsearch? if project.maintaining_elasticsearch?
project.maintain_elasticsearch_update project.maintain_elasticsearch_update
ElasticAssociationIndexerWorker.perform_async(self.project.class.name, project_id, ['issues']) if elasticsearch_project_associations_need_updating? associations_to_update = []
associations_to_update << 'issues' if elasticsearch_project_issues_need_updating?
associations_to_update << 'notes' if elasticsearch_project_notes_need_updating?
ElasticAssociationIndexerWorker.perform_async(self.project.class.name, project_id, associations_to_update) if associations_to_update.any?
end end
end end
...@@ -22,7 +32,11 @@ module EE ...@@ -22,7 +32,11 @@ module EE
private private
def elasticsearch_project_associations_need_updating? def elasticsearch_project_notes_need_updating?
self.previous_changes.keys.any? { |key| NOTES_PERMISSION_TRACKED_FIELDS.include?(key) }
end
def elasticsearch_project_issues_need_updating?
self.previous_changes.key?(:issues_access_level) self.previous_changes.key?(:issues_access_level)
end end
end end
......
...@@ -54,6 +54,22 @@ module Elastic ...@@ -54,6 +54,22 @@ module Elastic
nil nil
end end
# protect against missing project_feature and set visibility to PRIVATE
# if the project_feature is missing on a project
def safely_read_project_feature_for_elasticsearch(feature)
if target.project.project_feature
target.project.project_feature.access_level(feature)
else
logger.warn(
message: 'Project is missing ProjectFeature',
project_id: target.project_id,
id: target.id,
class: target.class
)
ProjectFeature::PRIVATE
end
end
def apply_field_limit(result) def apply_field_limit(result)
return result unless result.is_a? String return result unless result.is_a? String
......
...@@ -14,15 +14,7 @@ module Elastic ...@@ -14,15 +14,7 @@ module Elastic
data['assignee_id'] = safely_read_attribute_for_elasticsearch(:assignee_ids) data['assignee_id'] = safely_read_attribute_for_elasticsearch(:assignee_ids)
data['visibility_level'] = target.project.visibility_level data['visibility_level'] = target.project.visibility_level
data['issues_access_level'] = safely_read_project_feature_for_elasticsearch(:issues)
# protect against missing project_feature and set visibility to PRIVATE
# if the project_feature is missing on a project
begin
data['issues_access_level'] = target.project.project_feature.issues_access_level
rescue NoMethodError => e
Gitlab::ErrorTracking.track_exception(e, project_id: target.project_id, issue_id: target.id)
data['issues_access_level'] = ProjectFeature::PRIVATE
end
data.merge(generic_attributes) data.merge(generic_attributes)
end end
......
...@@ -22,8 +22,35 @@ module Elastic ...@@ -22,8 +22,35 @@ module Elastic
} }
end end
# do not add the permission fields unless the `remove_permissions_data_from_notes_documents`
# migration has completed otherwise the migration will never finish
if Elastic::DataMigrationService.migration_has_finished?(:remove_permissions_data_from_notes_documents)
data['visibility_level'] = target.project.visibility_level
merge_project_feature_access_level(data, noteable)
end
data.merge(generic_attributes) data.merge(generic_attributes)
end end
private
def merge_project_feature_access_level(data, noteable)
return unless noteable
case noteable
when Snippet
data['snippets_access_level'] = safely_read_project_feature_for_elasticsearch(:snippets)
when Commit
data['repository_access_level'] = safely_read_project_feature_for_elasticsearch(:repository)
when Issue, MergeRequest
access_level_attribute = ProjectFeature.access_level_attribute(noteable)
data[access_level_attribute.to_s] = safely_read_project_feature_for_elasticsearch(noteable)
else
# do nothing for other note types (DesignManagement::Design, AlertManagement::Alert, Epic, Vulnerability )
# are indexed but not currently searchable so we will not add permission
# data for them until the search capability is implemented
end
end
end end
end end
end end
...@@ -46,6 +46,13 @@ RSpec.describe RemovePermissionsDataFromNotesDocuments, :elastic, :sidekiq_inlin ...@@ -46,6 +46,13 @@ RSpec.describe RemovePermissionsDataFromNotesDocuments, :elastic, :sidekiq_inlin
context 'migration process' do context 'migration process' do
before do before do
add_permission_data_for_notes([note_on_commit, note_on_issue, note_on_merge_request, note_on_snippet]) add_permission_data_for_notes([note_on_commit, note_on_issue, note_on_merge_request, note_on_snippet])
allow(Elastic::DataMigrationService).to receive(:migration_has_finished?).and_call_original
# migrations are completed by default in test environments
# required to prevent the `as_indexed_json` method from populating the permissions fields
allow(Elastic::DataMigrationService).to receive(:migration_has_finished?)
.with(:remove_permissions_data_from_notes_documents)
.and_return(false)
end end
it 'queues documents for indexing' do it 'queues documents for indexing' do
......
...@@ -132,12 +132,11 @@ RSpec.describe Issue, :elastic do ...@@ -132,12 +132,11 @@ RSpec.describe Issue, :elastic do
expect(issue.__elasticsearch__.as_indexed_json).to eq(expected_hash) expect(issue.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
it 'handles a project missing project_feature' do it 'handles a project missing project_feature', :aggregate_failures do
issue = create :issue, project: project issue = create :issue, project: project
allow(issue.project).to receive(:project_feature).and_return(nil) allow(issue.project).to receive(:project_feature).and_return(nil)
expect(Gitlab::ErrorTracking).to receive(:track_exception) expect { issue.__elasticsearch__.as_indexed_json }.not_to raise_error
expect(issue.__elasticsearch__.as_indexed_json['issues_access_level']).to eq(ProjectFeature::PRIVATE) expect(issue.__elasticsearch__.as_indexed_json['issues_access_level']).to eq(ProjectFeature::PRIVATE)
end end
......
...@@ -85,34 +85,102 @@ RSpec.describe Note, :elastic do ...@@ -85,34 +85,102 @@ RSpec.describe Note, :elastic do
expect(described_class.elastic_search('term', options: options).total_count).to eq(4) expect(described_class.elastic_search('term', options: options).total_count).to eq(4)
end end
it "returns json with all needed elements" do describe 'json serialization' do
assignee = create(:user) using RSpec::Parameterized::TableSyntax
issue = create(:issue, assignees: [assignee])
note = create(:note, noteable: issue, project: issue.project) it "returns json with all needed elements" do
assignee = create(:user)
expected_hash = note.attributes.extract!( project = create(:project)
'id', issue = create(:issue, project: project, assignees: [assignee])
'note', note = create(:note, noteable: issue, project: project)
'project_id',
'noteable_type', expected_hash = note.attributes.extract!(
'noteable_id', 'id',
'created_at', 'note',
'updated_at', 'project_id',
'confidential' 'noteable_type',
).merge({ 'noteable_id',
'issue' => { 'created_at',
'assignee_id' => issue.assignee_ids, 'updated_at',
'author_id' => issue.author_id, 'confidential'
'confidential' => issue.confidential ).merge({
}, 'issue' => {
'type' => note.es_type, 'assignee_id' => issue.assignee_ids,
'join_field' => { 'author_id' => issue.author_id,
'name' => note.es_type, 'confidential' => issue.confidential
'parent' => note.es_parent },
} 'type' => note.es_type,
}) 'join_field' => {
'name' => note.es_type,
expect(note.__elasticsearch__.as_indexed_json).to eq(expected_hash) 'parent' => note.es_parent
},
'visibility_level' => project.visibility_level,
'issues_access_level' => project.issues_access_level
})
expect(note.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end
it 'does not raise error for notes with null noteable references' do
note = create(:note_on_issue)
allow(note).to receive(:noteable).and_return(nil)
expect { note.__elasticsearch__.as_indexed_json }.not_to raise_error
end
where(:note_type, :permission, :access_level) do
:note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:note_on_personal_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:diff_note_on_design | ProjectFeature::ENABLED | false
:legacy_diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:legacy_diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:note_on_alert | ProjectFeature::PRIVATE | false
:note_on_design | ProjectFeature::ENABLED | false
:note_on_epic | ProjectFeature::ENABLED | false
:note_on_vulnerability | ProjectFeature::PRIVATE | false
:discussion_note_on_vulnerability | ProjectFeature::PRIVATE | false
:discussion_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:discussion_note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:discussion_note_on_personal_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:track_mr_picking_note | ProjectFeature::PUBLIC | 'merge_requests_access_level'
end
with_them do
let_it_be(:project) { create(:project, :repository) }
let!(:note) { create(note_type, project: project) } # rubocop:disable Rails/SaveBang
let(:note_json) { note.__elasticsearch__.as_indexed_json }
before do
project.project_feature.update_attribute(access_level.to_sym, permission) if access_level.present?
end
it 'does not contain permissions if remove_permissions_data_from_notes_documents is not finished' do
allow(Elastic::DataMigrationService).to receive(:migration_has_finished?)
.with(:remove_permissions_data_from_notes_documents)
.and_return(false)
expect(note_json).not_to have_key(access_level) if access_level.present?
expect(note_json).not_to have_key('visibility_level')
end
it 'contains the correct permissions', :aggregate_failures do
if access_level
expect(note_json).to have_key(access_level)
expect(note_json[access_level]).to eq(permission)
end
expect(note_json).to have_key('visibility_level')
expect(note_json['visibility_level']).to eq(project.visibility_level)
end
end
end end
it 'does not track system note updates' do it 'does not track system note updates' do
......
...@@ -28,13 +28,16 @@ RSpec.describe ProjectFeature do ...@@ -28,13 +28,16 @@ RSpec.describe ProjectFeature do
allow(project).to receive(:maintaining_elasticsearch?).and_return(true) allow(project).to receive(:maintaining_elasticsearch?).and_return(true)
end end
where(:feature, :worker_expected) do where(:feature, :worker_expected, :associations) do
'issues' | true 'issues' | true | %w[issues notes]
'wiki' | false 'wiki' | false | nil
'builds' | false 'builds' | false | nil
'merge_requests' | false 'merge_requests' | true | %w[notes]
'repository' | false 'repository' | true | %w[notes]
'pages' | false 'snippets' | true | %w[notes]
'operations' | false | nil
'security_and_compliance' | false | nil
'pages' | false | nil
end end
with_them do with_them do
...@@ -42,12 +45,12 @@ RSpec.describe ProjectFeature do ...@@ -42,12 +45,12 @@ RSpec.describe ProjectFeature do
expect(project).to receive(:maintain_elasticsearch_update) expect(project).to receive(:maintain_elasticsearch_update)
if worker_expected if worker_expected
expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, ['issues']) expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, associations)
else else
expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async) expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async)
end end
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
end end
end end
end end
......
...@@ -2707,8 +2707,8 @@ RSpec.describe Project do ...@@ -2707,8 +2707,8 @@ RSpec.describe Project do
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
context 'when updating the visibility_level' do context 'when updating the visibility_level' do
it 'triggers ElasticAssociationIndexerWorker to update issues' do it 'triggers ElasticAssociationIndexerWorker to update issues and notes' do
expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, ['issues']) expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, %w[issues notes])
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end end
......
...@@ -67,11 +67,11 @@ RSpec.describe Groups::TransferService, '#execute' do ...@@ -67,11 +67,11 @@ RSpec.describe Groups::TransferService, '#execute' do
expect(::Gitlab::CurrentSettings).not_to receive(:invalidate_elasticsearch_indexes_cache_for_project!) expect(::Gitlab::CurrentSettings).not_to receive(:invalidate_elasticsearch_indexes_cache_for_project!)
expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project1) expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project1)
expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project1.id, ['issues']) expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project1.id, %w[issues notes])
expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project2) expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project2)
expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project2.id, ['issues']) expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project2.id, %w[issues notes])
expect(Elastic::ProcessBookkeepingService).not_to receive(:track!).with(project3) expect(Elastic::ProcessBookkeepingService).not_to receive(:track!).with(project3)
expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async).with('Project', project3.id, ['issues']) expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async).with('Project', project3.id, %w[issues notes])
transfer_service.execute(new_group) transfer_service.execute(new_group)
......
...@@ -72,9 +72,9 @@ RSpec.describe Projects::TransferService do ...@@ -72,9 +72,9 @@ RSpec.describe Projects::TransferService do
context 'when visibility level changes' do context 'when visibility level changes' do
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private) }
it 'reindexes the project and associated issues' do it 'reindexes the project and associated issues and notes' do
expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project) expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(project)
expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, ['issues']) expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, %w[issues notes])
subject.execute(group) subject.execute(group)
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