From 3a5df1d8fc518900d8e33a6be8a2243e399c754a Mon Sep 17 00:00:00 2001
From: Robert Speicher <robert@gitlab.com>
Date: Tue, 3 Jan 2017 18:03:13 +0000
Subject: [PATCH] Merge branch 'fix-api-mr-permissions' into 'security'

Ensure that only privileged users can access merge requests in the API

See merge request !2053
---
 .../unreleased/fix-api-mr-permissions.yml     |  4 +++
 lib/api/helpers.rb                            |  6 ++++
 lib/api/merge_request_diffs.rb                |  8 ++---
 lib/api/merge_requests.rb                     | 25 +++++++---------
 lib/api/subscriptions.rb                      |  4 +--
 lib/api/todos.rb                              |  2 +-
 spec/requests/api/merge_requests_spec.rb      | 29 +++++++++++++++++++
 spec/requests/api/todos_spec.rb               | 15 +++++++++-
 8 files changed, 68 insertions(+), 25 deletions(-)
 create mode 100644 changelogs/unreleased/fix-api-mr-permissions.yml

diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml
new file mode 100644
index 00000000000..33b677b1f29
--- /dev/null
+++ b/changelogs/unreleased/fix-api-mr-permissions.yml
@@ -0,0 +1,4 @@
+---
+title: Don't allow project guests to subscribe to merge requests through the API
+merge_request:
+author: Robert Schilling
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 49c5f0652ab..a1d7b323f4f 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -90,6 +90,12 @@ module API
       MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
     end
 
+    def find_merge_request_with_access(id, access_level = :read_merge_request)
+      merge_request = user_project.merge_requests.find(id)
+      authorize! access_level, merge_request
+      merge_request
+    end
+
     def authenticate!
       unauthorized! unless current_user
     end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 07435d78468..bc3d69f6904 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -15,10 +15,8 @@ module API
       end
 
       get ":id/merge_requests/:merge_request_id/versions" do
-        merge_request = user_project.merge_requests.
-          find(params[:merge_request_id])
+        merge_request = find_merge_request_with_access(params[:merge_request_id])
 
-        authorize! :read_merge_request, merge_request
         present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
       end
 
@@ -34,10 +32,8 @@ module API
       end
 
       get ":id/merge_requests/:merge_request_id/versions/:version_id" do
-        merge_request = user_project.merge_requests.
-          find(params[:merge_request_id])
+        merge_request = find_merge_request_with_access(params[:merge_request_id])
 
-        authorize! :read_merge_request, merge_request
         present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
       end
     end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index e77af4b7a0d..7ffb38e62da 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -118,8 +118,8 @@ module API
           success Entities::MergeRequest
         end
         get path do
-          merge_request = find_project_merge_request(params[:merge_request_id])
-          authorize! :read_merge_request, merge_request
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
+
           present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
         end
 
@@ -127,8 +127,8 @@ module API
           success Entities::RepoCommit
         end
         get "#{path}/commits" do
-          merge_request = find_project_merge_request(params[:merge_request_id])
-          authorize! :read_merge_request, merge_request
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
+
           present merge_request.commits, with: Entities::RepoCommit
         end
 
@@ -136,8 +136,8 @@ module API
           success Entities::MergeRequestChanges
         end
         get "#{path}/changes" do
-          merge_request = find_project_merge_request(params[:merge_request_id])
-          authorize! :read_merge_request, merge_request
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
+
           present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
         end
 
@@ -155,8 +155,7 @@ module API
                           :remove_source_branch
         end
         put path do
-          merge_request = find_project_merge_request(params.delete(:merge_request_id))
-          authorize! :update_merge_request, merge_request
+          merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
 
           mr_params = declared_params(include_missing: false)
           mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
@@ -235,10 +234,7 @@ module API
           use :pagination
         end
         get "#{path}/comments" do
-          merge_request = find_project_merge_request(params[:merge_request_id])
-
-          authorize! :read_merge_request, merge_request
-
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
           present paginate(merge_request.notes.fresh), with: Entities::MRNote
         end
 
@@ -250,8 +246,7 @@ module API
           requires :note, type: String, desc: 'The text of the comment'
         end
         post "#{path}/comments" do
-          merge_request = find_project_merge_request(params[:merge_request_id])
-          authorize! :create_note, merge_request
+          merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
 
           opts = {
             note: params[:note],
@@ -275,7 +270,7 @@ module API
           use :pagination
         end
         get "#{path}/closes_issues" do
-          merge_request = find_project_merge_request(params[:merge_request_id])
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
           issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
           present paginate(issues), with: issue_entity(user_project), current_user: current_user
         end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 10749b34004..e11d7537cc9 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,8 +3,8 @@ module API
     before { authenticate! }
 
     subscribable_types = {
-      'merge_request' => proc { |id| user_project.merge_requests.find(id) },
-      'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
+      'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
+      'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
       'issues' => proc { |id| find_project_issue(id) },
       'labels' => proc { |id| find_project_label(id) },
     }
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ed8f48aa1e3..9bd077263a7 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,7 +5,7 @@ module API
     before { authenticate! }
 
     ISSUABLE_TYPES = {
-      'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
+      'merge_requests' => ->(id) { find_merge_request_with_access(id) },
       'issues' => ->(id) { find_project_issue(id) }
     }
 
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 6f20ac49269..71a7994e544 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -627,6 +627,17 @@ describe API::MergeRequests, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
       expect(json_response.first['id']).to eq(issue.id)
     end
+
+    it 'returns 403 if the user has no access to the merge request' do
+      project = create(:empty_project, :private)
+      merge_request = create(:merge_request, :simple, source_project: project)
+      guest = create(:user)
+      project.team << [guest, :guest]
+
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+      expect(response).to have_http_status(403)
+    end
   end
 
   describe 'POST :id/merge_requests/:merge_request_id/subscription' do
@@ -648,6 +659,15 @@ describe API::MergeRequests, api: true  do
 
       expect(response).to have_http_status(404)
     end
+
+    it 'returns 403 if user has no access to read code' do
+      guest = create(:user)
+      project.team << [guest, :guest]
+
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+      expect(response).to have_http_status(403)
+    end
   end
 
   describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
@@ -669,6 +689,15 @@ describe API::MergeRequests, api: true  do
 
       expect(response).to have_http_status(404)
     end
+
+    it 'returns 403 if user has no access to read code' do
+      guest = create(:user)
+      project.team << [guest, :guest]
+
+      delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+      expect(response).to have_http_status(403)
+    end
   end
 
   describe 'Time tracking' do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 6fe695626ca..56dc017ce54 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -183,12 +183,25 @@ describe API::Todos, api: true do
 
       expect(response.status).to eq(404)
     end
+
+    it 'returns an error if the issuable is not accessible' do
+      guest = create(:user)
+      project_1.team << [guest, :guest]
+
+      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
+
+      if issuable_type == 'merge_requests'
+        expect(response).to have_http_status(403)
+      else
+        expect(response).to have_http_status(404)
+      end
+    end
   end
 
   describe 'POST :id/issuable_type/:issueable_id/todo' do
     context 'for an issue' do
       it_behaves_like 'an issuable', 'issues' do
-        let(:issuable) { create(:issue, author: author_1, project: project_1) }
+        let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) }
       end
     end
 
-- 
GitLab