From b092924c4afacfc161ec35f91f48a17fc94190d7 Mon Sep 17 00:00:00 2001
From: Adam Hegyi <ahegyi@gitlab.com>
Date: Wed, 4 Dec 2019 16:35:46 +0100
Subject: [PATCH] Enforce permission check when counting events

- Call `visible_to_user?` when counting events
- Caching `visible_to_user?` to reduce the number of queries
- Add ee factory for events to create valid group event
---
 app/controllers/dashboard_controller.rb       |  3 +-
 app/controllers/groups_controller.rb          |  7 ++--
 app/controllers/projects_controller.rb        |  3 +-
 app/presenters/event_presenter.rb             | 12 ++++++
 ...nforce-permissions-for-event-filter-ee.yml |  5 +++
 .../controllers/ee/groups_controller_spec.rb  |  6 +--
 ee/spec/factories/events.rb                   | 13 ++++++
 spec/controllers/dashboard_controller_spec.rb | 41 +++++++++++++++++++
 spec/controllers/groups_controller_spec.rb    | 37 ++++++++++++++---
 spec/controllers/projects_controller_spec.rb  | 40 ++++++++++++++++++
 10 files changed, 153 insertions(+), 14 deletions(-)
 create mode 100644 changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml
 create mode 100644 ee/spec/factories/events.rb

diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 1a97b39d3ae1..1668cf004f80 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -19,7 +19,7 @@ def activity
 
       format.json do
         load_events
-        pager_json("events/_events", @events.count)
+        pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
       end
     end
   end
@@ -37,6 +37,7 @@ def load_events
     @events = EventCollection
       .new(projects, offset: params[:offset].to_i, filter: event_filter)
       .to_a
+      .map(&:present)
 
     Events::RenderService.new(current_user).execute(@events)
   end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0953ca963175..958dc27984f4 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -91,7 +91,7 @@ def activity
 
       format.json do
         load_events
-        pager_json("events/_events", @events.count)
+        pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) })
       end
     end
   end
@@ -209,8 +209,9 @@ def load_events
                  .includes(:namespace)
 
     @events = EventCollection
-                .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
-                .to_a
+      .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
+      .to_a
+      .map(&:present)
 
     Events::RenderService
       .new(current_user)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index bf05defbc2e6..9d0901881d3e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -118,7 +118,7 @@ def activity
       format.html
       format.json do
         load_events
-        pager_json('events/_events', @events.count)
+        pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
       end
     end
   end
@@ -343,6 +343,7 @@ def load_events
     @events = EventCollection
       .new(projects, offset: params[:offset].to_i, filter: event_filter)
       .to_a
+      .map(&:present)
 
     Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
   end
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index f31d362d5fa7..5657e0b96bce 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -3,6 +3,18 @@
 class EventPresenter < Gitlab::View::Presenter::Delegated
   presents :event
 
+  def initialize(subject, **attributes)
+    super
+
+    @visible_to_user_cache = ActiveSupport::Cache::MemoryStore.new
+  end
+
+  # Caching `visible_to_user?` method in the presenter beause it might be called multiple times.
+  def visible_to_user?(user = nil)
+    @visible_to_user_cache.fetch(user&.id) { super(user) }
+  end
+
+  # implement cache here
   def resource_parent_name
     resource_parent&.full_name || ''
   end
diff --git a/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml b/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml
new file mode 100644
index 000000000000..7d74d6108f87
--- /dev/null
+++ b/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml
@@ -0,0 +1,5 @@
+---
+title: Enforce permission check when counting activity events
+merge_request:
+author:
+type: security
diff --git a/ee/spec/controllers/ee/groups_controller_spec.rb b/ee/spec/controllers/ee/groups_controller_spec.rb
index 939920a0fb4e..4dc696a155c5 100644
--- a/ee/spec/controllers/ee/groups_controller_spec.rb
+++ b/ee/spec/controllers/ee/groups_controller_spec.rb
@@ -15,9 +15,9 @@
     render_views
 
     let_it_be(:event1) { create(:event, project: project) }
-    let_it_be(:event2) { create(:event, project: nil, group: group) }
-    let_it_be(:event3) { create(:event, project: nil, group: subgroup) }
-    let_it_be(:event4) { create(:event, project: nil, group: subgroup2) }
+    let_it_be(:event2) { create(:event, :epic_create_event, group: group) }
+    let_it_be(:event3) { create(:event, :epic_create_event, group: subgroup) }
+    let_it_be(:event4) { create(:event, :epic_create_event, group: subgroup2) }
 
     context 'when authorized' do
       before do
diff --git a/ee/spec/factories/events.rb b/ee/spec/factories/events.rb
new file mode 100644
index 000000000000..9c6f15ed47e3
--- /dev/null
+++ b/ee/spec/factories/events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.modify do
+  factory :event do
+    trait :epic_create_event do
+      group
+      author(factory: :user)
+      target(factory: :epic)
+      action { Event::CREATED }
+      project { nil }
+    end
+  end
+end
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index a733c3ecaa1f..305419efe969 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -23,6 +23,47 @@
     end
   end
 
+  describe "GET activity as JSON" do
+    render_views
+
+    let(:user) { create(:user) }
+    let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
+
+    before do
+      create(:event, :created, project: project, target: create(:issue))
+
+      sign_in(user)
+
+      request.cookies[:event_filter] = 'all'
+    end
+
+    context 'when user has permission to see the event' do
+      before do
+        project.add_developer(user)
+      end
+
+      it 'returns count' do
+        get :activity, params: { format: :json }
+
+        expect(json_response['count']).to eq(1)
+      end
+    end
+
+    context 'when user has no permission to see the event' do
+      it 'filters out invisible event' do
+        get :activity, params: { format: :json }
+
+        expect(json_response['html']).to include(_('No activities found'))
+      end
+
+      it 'filters out invisible event when calculating the count' do
+        get :activity, params: { format: :json }
+
+        expect(json_response['count']).to eq(0)
+      end
+    end
+  end
+
   it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
   it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
 
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 2ed2b3192988..ddfd2b424e76 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -47,7 +47,7 @@
 
       it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do
         subject
-        expect(assigns(:events)).to contain_exactly(event)
+        expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
       end
     end
   end
@@ -119,12 +119,12 @@
   describe 'GET #activity' do
     render_views
 
-    before do
-      sign_in(user)
-      project
-    end
-
     context 'as json' do
+      before do
+        sign_in(user)
+        project
+      end
+
       it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do
         2.times do
           project = create(:project, group: group)
@@ -141,6 +141,31 @@
         expect(assigns(:projects).limit_value).to be_nil
       end
     end
+
+    context 'when user has no permission to see the event' do
+      let(:user) { create(:user) }
+      let(:group) { create(:group) }
+      let(:project) { create(:project, group: group) }
+
+      let(:project_with_restricted_access) do
+        create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
+      end
+
+      before do
+        create(:event, project: project)
+        create(:event, :created, project: project_with_restricted_access, target: create(:issue))
+
+        group.add_guest(user)
+
+        sign_in(user)
+      end
+
+      it 'filters out invisible event' do
+        get :activity, params: { id: group.to_param }, format: :json
+
+        expect(json_response['count']).to eq(1)
+      end
+    end
   end
 
   describe 'POST #create' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 9ae1277de26b..3870ef5d947d 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -64,6 +64,46 @@
     end
   end
 
+  describe "GET #activity as JSON" do
+    render_views
+
+    let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
+
+    before do
+      create(:event, :created, project: project, target: create(:issue))
+
+      sign_in(user)
+
+      request.cookies[:event_filter] = 'all'
+    end
+
+    context 'when user has permission to see the event' do
+      before do
+        project.add_developer(user)
+      end
+
+      it 'returns count' do
+        get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
+
+        expect(json_response['count']).to eq(1)
+      end
+    end
+
+    context 'when user has no permission to see the event' do
+      it 'filters out invisible event' do
+        get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
+
+        expect(json_response['html']).to eq("\n")
+      end
+
+      it 'filters out invisible event when calculating the count' do
+        get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
+
+        expect(json_response['count']).to eq(0)
+      end
+    end
+  end
+
   describe "GET show" do
     context "user not project member" do
       before do
-- 
GitLab