From 9273ca68ea1e8f75ae29b073dc707042f3294809 Mon Sep 17 00:00:00 2001
From: Patrick Derichs <pderichs@gitlab.com>
Date: Tue, 20 Aug 2019 09:19:20 +0200
Subject: [PATCH] Filter out old system notes for epics

Add spec for note filtering on epics
---
 app/controllers/concerns/issuable_actions.rb  |  2 +-
 app/controllers/concerns/notes_actions.rb     |  2 +-
 app/models/note.rb                            |  4 ++
 ee/app/models/ee/note.rb                      | 21 ++++++++
 ...-api-reveals-historical-info-ee-master.yml |  5 ++
 ee/lib/api/v3/github.rb                       |  2 +-
 .../lib/ee/api/helpers/notes_helpers_spec.rb  | 45 ++++++++++++++++
 ee/spec/models/note_spec.rb                   | 54 +++++++++++++++++++
 ee/spec/requests/api/notes_spec.rb            | 32 +++++++++++
 lib/api/discussions.rb                        |  2 +-
 lib/api/helpers/notes_helpers.rb              |  6 +--
 lib/api/notes.rb                              |  2 +-
 12 files changed, 169 insertions(+), 8 deletions(-)
 create mode 100644 ee/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ee-master.yml
 create mode 100644 ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb

diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index d920a31027c..bf5384d5ac8 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -110,7 +110,7 @@ module IssuableActions
     end
 
     notes = prepare_notes_for_rendering(notes)
-    notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+    notes = notes.select { |n| n.visible_for?(current_user) }
 
     discussions = Discussion.build_collection(notes, issuable)
 
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index d2a961efff7..80be7095ed3 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -29,7 +29,7 @@ module NotesActions
     end
 
     notes = prepare_notes_for_rendering(notes)
-    notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+    notes = notes.select { |n| n.visible_for?(current_user) }
 
     notes_json[:notes] =
       if use_note_serializer?
diff --git a/app/models/note.rb b/app/models/note.rb
index 215e8883691..c31c514c160 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -331,6 +331,10 @@ class Note < ApplicationRecord
     cross_reference? && !all_referenced_mentionables_allowed?(user)
   end
 
+  def visible_for?(user)
+    !cross_reference_not_visible_for?(user)
+  end
+
   def award_emoji?
     can_be_award_emoji? && contains_emoji_only?
   end
diff --git a/ee/app/models/ee/note.rb b/ee/app/models/ee/note.rb
index acb153850e4..cbffbdc2b03 100644
--- a/ee/app/models/ee/note.rb
+++ b/ee/app/models/ee/note.rb
@@ -56,8 +56,29 @@ module EE
       for_epic? ? noteable.group : super
     end
 
+    override :visible_for?
+    def visible_for?(user)
+      return false unless super
+
+      return true unless system_note_for_epic? && created_before_noteable?
+
+      group_reporter?(user, noteable.group)
+    end
+
     private
 
+    def system_note_for_epic?
+      for_epic? && system?
+    end
+
+    def created_before_noteable?
+      created_at.to_i < noteable.created_at.to_i
+    end
+
+    def group_reporter?(user, group)
+      group.max_member_access_for_user(user) >= ::Gitlab::Access::REPORTER
+    end
+
     def banzai_context_params
       { group: noteable.group, label_url_method: :group_epics_url }
     end
diff --git a/ee/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ee-master.yml b/ee/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ee-master.yml
new file mode 100644
index 00000000000..c639098721e
--- /dev/null
+++ b/ee/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ee-master.yml
@@ -0,0 +1,5 @@
+---
+title: Filter out old system notes for epics in notes api endpoint response
+merge_request:
+author:
+type: security
diff --git a/ee/lib/api/v3/github.rb b/ee/lib/api/v3/github.rb
index 80c568067dd..6645249146d 100644
--- a/ee/lib/api/v3/github.rb
+++ b/ee/lib/api/v3/github.rb
@@ -83,7 +83,7 @@ module API
           # They're not presented on Jira Dev Panel ATM. A comments count with a
           # redirect link is presented.
           notes = paginate(noteable.notes.user.reorder(nil))
-          notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+          notes.select { |n| n.visible_for?(current_user) }
         end
         # rubocop: enable CodeReuse/ActiveRecord
 
diff --git a/ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb b/ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb
new file mode 100644
index 00000000000..b11c2231330
--- /dev/null
+++ b/ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe 'NotesHelpers' do
+  describe '#find_noteable' do
+    let!(:group) { create(:group, :public) }
+    let!(:other_group) { create(:group, :public) }
+    let!(:project) { create(:project, :public, namespace: group) }
+    let!(:user) { create(:group_member, :owner, group: group, user: create(:user)).user }
+    let!(:epic) { create(:epic, author: user, group: group) }
+    let!(:parent_id) { group.id }
+    let!(:noteable_type) { Epic }
+
+    let(:klazz) do
+      klazz = Class.new do
+        def initialize(user)
+          @user = user
+        end
+
+        def current_user
+          @user
+        end
+
+        def can?(user, ability, noteable)
+          user == @user && ability == :read_epic
+        end
+      end
+
+      klazz.prepend(API::Helpers::NotesHelpers)
+    end
+
+    let(:subject) { klazz.new(user) }
+
+    before do
+      stub_licensed_features(epics: true)
+    end
+
+    it 'returns the expected epic' do
+      expect(subject.find_noteable(Group, parent_id, noteable_type, epic.id)).to eq(epic)
+    end
+
+    it 'raises not found exception when epic does not belong to group' do
+      expect { subject.find_noteable(Group, other_group.id, noteable_type, epic.id) }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+end
diff --git a/ee/spec/models/note_spec.rb b/ee/spec/models/note_spec.rb
index 29c71ad584c..3bd9c4fb149 100644
--- a/ee/spec/models/note_spec.rb
+++ b/ee/spec/models/note_spec.rb
@@ -10,4 +10,58 @@ describe Note do
     let(:backref_text) { issue.gfm_reference }
     let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
   end
+
+  describe '#visible_for?' do
+    let(:owner) { create(:group_member, :owner, group: group, user: create(:user)).user }
+    let(:guest) { create(:group_member, :guest, group: group, user: create(:user)).user }
+    let(:reporter) { create(:group_member, :reporter, group: group, user: create(:user)).user }
+    let(:maintainer) { create(:group_member, :maintainer, group: group, user: create(:user)).user }
+
+    let(:group) { create(:group) }
+    let(:epic) { create(:epic, group: group, author: owner, created_at: 1.day.ago) }
+
+    before do
+      stub_licensed_features(epics: true)
+    end
+
+    context 'note created after epic' do
+      let(:note) { create(:system_note, noteable: epic, created_at: 1.minute.ago) }
+
+      it 'returns true for an owner' do
+        expect(note.visible_for?(owner)).to be_truthy
+      end
+
+      it 'returns true for a reporter' do
+        expect(note.visible_for?(reporter)).to be_truthy
+      end
+
+      it 'returns true for a maintainer' do
+        expect(note.visible_for?(maintainer)).to be_truthy
+      end
+
+      it 'returns true for a guest user' do
+        expect(note.visible_for?(guest)).to be_truthy
+      end
+    end
+
+    context 'when note is older than epic' do
+      let(:older_note) { create(:system_note, noteable: epic, created_at: 2.days.ago) }
+
+      it 'returns true for the owner' do
+        expect(older_note.visible_for?(owner)).to be_truthy
+      end
+
+      it 'returns true for a reporter' do
+        expect(older_note.visible_for?(reporter)).to be_truthy
+      end
+
+      it 'returns true for a maintainer' do
+        expect(older_note.visible_for?(maintainer)).to be_truthy
+      end
+
+      it 'returns false for a guest user' do
+        expect(older_note.visible_for?(guest)).to be_falsy
+      end
+    end
+  end
 end
diff --git a/ee/spec/requests/api/notes_spec.rb b/ee/spec/requests/api/notes_spec.rb
index b85a4d50741..b87b27c563a 100644
--- a/ee/spec/requests/api/notes_spec.rb
+++ b/ee/spec/requests/api/notes_spec.rb
@@ -26,5 +26,37 @@ describe API::Notes do
       let(:noteable) { epic }
       let(:note) { epic_note }
     end
+
+    context 'when issue was promoted to epic' do
+      let!(:promoted_issue_epic) { create(:epic, group: group, author: owner, created_at: 1.day.ago) }
+      let!(:owner) { create(:group_member, :owner, user: create(:user), group: group).user }
+      let!(:reporter) { create(:group_member, :reporter, user: create(:user), group: group).user }
+      let!(:guest) { create(:group_member, :guest, user: create(:user), group: group).user }
+      let!(:previous_note) { create(:note, :system, noteable: promoted_issue_epic, created_at: 2.days.ago) }
+      let!(:previous_note2) { create(:note, :system, noteable: promoted_issue_epic, created_at: 2.minutes.ago) }
+      let!(:epic_note) { create(:note, noteable: promoted_issue_epic, author: owner) }
+
+      context 'when user is reporter' do
+        it 'returns previous issue system notes' do
+          get api("/groups/#{group.id}/epics/#{promoted_issue_epic.id}/notes", reporter)
+
+          expect(response).to have_gitlab_http_status(200)
+          expect(response).to include_pagination_headers
+          expect(json_response).to be_an Array
+          expect(json_response.size).to eq(3)
+        end
+      end
+
+      context 'when user is guest' do
+        it 'does not return previous issue system notes' do
+          get api("/groups/#{group.id}/epics/#{promoted_issue_epic.id}/notes", guest)
+
+          expect(response).to have_gitlab_http_status(200)
+          expect(response).to include_pagination_headers
+          expect(json_response).to be_an Array
+          expect(json_response.size).to eq(2)
+        end
+      end
+    end
   end
 end
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 6c1acc3963f..9125207167c 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -239,7 +239,7 @@ module API
         # because notes are redacted if they point to projects that
         # cannot be accessed by the user.
         notes = prepare_notes_for_rendering(notes)
-        notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+        notes.select { |n| n.visible_for?(current_user) }
       end
       # rubocop: enable CodeReuse/ActiveRecord
     end
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index c938cb8a592..8adfac346f6 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -12,7 +12,7 @@ module API
       end
 
       def update_note(noteable, note_id)
-        note = noteable.notes.find(params[:note_id])
+        note = noteable.notes.find(note_id)
 
         authorize! :admin_note, note
 
@@ -61,8 +61,8 @@ module API
       end
 
       def get_note(noteable, note_id)
-        note = noteable.notes.with_metadata.find(params[:note_id])
-        can_read_note = !note.cross_reference_not_visible_for?(current_user)
+        note = noteable.notes.with_metadata.find(note_id)
+        can_read_note = note.visible_for?(current_user)
 
         if can_read_note
           present note, with: Entities::Note
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 84563d66ee8..16fca9acccb 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -42,7 +42,7 @@ module API
           # array returned, but this is really a edge-case.
           notes = paginate(raw_notes)
           notes = prepare_notes_for_rendering(notes)
-          notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+          notes = notes.select { |note| note.visible_for?(current_user) }
           present notes, with: Entities::Note
         end
         # rubocop: enable CodeReuse/ActiveRecord
-- 
2.30.9