From e122e14ac6a25c7813ca888a97bd4a3298e78d9d Mon Sep 17 00:00:00 2001 From: Cindy Pallares Date: Wed, 28 Nov 2018 19:04:15 +0000 Subject: [PATCH] Merge branch 'security-guest-comments' into 'master' [master]Fixed ability to comment on and edit/delete comments on locked or confidential issues See merge request gitlab/gitlabhq!2612 --- app/controllers/concerns/notes_actions.rb | 10 ++ app/mailers/emails/notes.rb | 2 +- app/models/note.rb | 2 +- app/policies/commit_policy.rb | 2 + app/policies/note_policy.rb | 9 ++ ...l => note_project_snippet_email.html.haml} | 0 ...rb => note_project_snippet_email.text.erb} | 0 .../unreleased/security-guest-comments.yml | 5 + .../unreleased/security-guest-comments_2.yml | 5 + .../projects/notes_controller_spec.rb | 105 +++++++++++++----- spec/mailers/notify_spec.rb | 2 +- spec/models/note_spec.rb | 2 +- spec/policies/note_policy_spec.rb | 79 ++++++++++++- 13 files changed, 188 insertions(+), 35 deletions(-) rename app/views/notify/{note_snippet_email.html.haml => note_project_snippet_email.html.haml} (100%) rename app/views/notify/{note_snippet_email.text.erb => note_project_snippet_email.text.erb} (100%) create mode 100644 changelogs/unreleased/security-guest-comments.yml create mode 100644 changelogs/unreleased/security-guest-comments_2.yml diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 777b147e2dd..0319948a12f 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -6,6 +6,7 @@ module NotesActions extend ActiveSupport::Concern included do + prepend_before_action :normalize_create_params, only: [:create] before_action :set_polling_interval_header, only: [:index] before_action :require_noteable!, only: [:index, :create] before_action :authorize_admin_note!, only: [:update, :destroy] @@ -247,6 +248,15 @@ module NotesActions DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity) end + # Avoids checking permissions in the wrong object - this ensures that the object we checked permissions for + # is the object we're actually creating a note in. + def normalize_create_params + params[:note].try do |note| + note[:noteable_id] = params[:target_id] + note[:noteable_type] = params[:target_type].classify + end + end + def note_project strong_memoize(:note_project) do next nil unless project diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index d3284e90568..1b3c1f9a8a9 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -26,7 +26,7 @@ module Emails mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id)) end - def note_snippet_email(recipient_id, note_id) + def note_project_snippet_email(recipient_id, note_id) setup_note_mail(note_id, recipient_id) @snippet = @note.noteable diff --git a/app/models/note.rb b/app/models/note.rb index 592efb714f3..a6ae4f58ac4 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -324,7 +324,7 @@ class Note < ActiveRecord::Base end def to_ability_name - for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore + for_snippet? ? noteable.class.name.underscore : noteable_type.underscore end def can_be_discussion_note? diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb index 67e9bc12804..4d4f0ba9267 100644 --- a/app/policies/commit_policy.rb +++ b/app/policies/commit_policy.rb @@ -2,4 +2,6 @@ class CommitPolicy < BasePolicy delegate { @subject.project } + + rule { can?(:download_code) }.enable :read_commit end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index bbc2b48b856..f22843b6463 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -9,8 +9,17 @@ class NotePolicy < BasePolicy condition(:editable, scope: :subject) { @subject.editable? } + condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") } + rule { ~editable }.prevent :admin_note + # If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes + rule { ~can_read_noteable }.policy do + prevent :read_note + prevent :admin_note + prevent :resolve_note + end + rule { is_author }.policy do enable :read_note enable :admin_note diff --git a/app/views/notify/note_snippet_email.html.haml b/app/views/notify/note_project_snippet_email.html.haml similarity index 100% rename from app/views/notify/note_snippet_email.html.haml rename to app/views/notify/note_project_snippet_email.html.haml diff --git a/app/views/notify/note_snippet_email.text.erb b/app/views/notify/note_project_snippet_email.text.erb similarity index 100% rename from app/views/notify/note_snippet_email.text.erb rename to app/views/notify/note_project_snippet_email.text.erb diff --git a/changelogs/unreleased/security-guest-comments.yml b/changelogs/unreleased/security-guest-comments.yml new file mode 100644 index 00000000000..2c99512433b --- /dev/null +++ b/changelogs/unreleased/security-guest-comments.yml @@ -0,0 +1,5 @@ +--- +title: Fixed ability to comment on locked/confidential issues. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-guest-comments_2.yml b/changelogs/unreleased/security-guest-comments_2.yml new file mode 100644 index 00000000000..be6f2d6a490 --- /dev/null +++ b/changelogs/unreleased/security-guest-comments_2.yml @@ -0,0 +1,5 @@ +--- +title: Fixed ability of guest users to edit/delete comments on locked or confidential issues. +merge_request: +author: +type: security diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 9ac7b8ee8a8..d2a26068362 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -283,14 +283,14 @@ describe Projects::NotesController do def post_create(extra_params = {}) post :create, { - note: { note: 'some other note' }, - namespace_id: project.namespace, - project_id: project, - target_type: 'merge_request', - target_id: merge_request.id, - note_project_id: forked_project.id, - in_reply_to_discussion_id: existing_comment.discussion_id - }.merge(extra_params) + note: { note: 'some other note', noteable_id: merge_request.id }, + namespace_id: project.namespace, + project_id: project, + target_type: 'merge_request', + target_id: merge_request.id, + note_project_id: forked_project.id, + in_reply_to_discussion_id: existing_comment.discussion_id + }.merge(extra_params) end context 'when the note_project_id is not correct' do @@ -324,6 +324,30 @@ describe Projects::NotesController do end end + context 'when target_id and noteable_id do not match' do + let(:locked_issue) { create(:issue, :locked, project: project) } + let(:issue) {create(:issue, project: project)} + + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + project.project_member(user).destroy + end + + it 'uses target_id and ignores noteable_id' do + request_params = { + note: { note: 'some note', noteable_type: 'Issue', noteable_id: locked_issue.id }, + target_type: 'issue', + target_id: issue.id, + project_id: project, + namespace_id: project.namespace + } + + expect { post :create, request_params }.to change { issue.notes.count }.by(1) + .and change { locked_issue.notes.count }.by(0) + expect(response).to have_gitlab_http_status(302) + end + end + context 'when the merge request discussion is locked' do before do project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) @@ -376,35 +400,60 @@ describe Projects::NotesController do end describe 'PUT update' do - let(:request_params) do - { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :json, - note: { - note: "New comment" + context "should update the note with a valid issue" do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } } - } - end + end - before do - sign_in(note.author) - project.add_developer(note.author) - end + before do + sign_in(note.author) + project.add_developer(note.author) + end - it "updates the note" do - expect { put :update, request_params }.to change { note.reload.note } + it "updates the note" do + expect { put :update, request_params }.to change { note.reload.note } + end + end + context "doesnt update the note" do + let(:issue) { create(:issue, :confidential, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } + + before do + sign_in(user) + project.add_guest(user) + end + + it "disallows edits when the issue is confidential and the user has guest permissions" do + request_params = { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } + } + expect { put :update, request_params }.not_to change { note.reload.note } + expect(response).to have_gitlab_http_status(404) + end end end describe 'DELETE destroy' do let(:request_params) do { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :js + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js } end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ff1a5aa2536..150c00e4bfe 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -522,7 +522,7 @@ describe Notify do let(:project_snippet) { create(:project_snippet, project: project) } let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) } - subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } + subject { described_class.note_project_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { project_snippet } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index f9be61e4768..bcdfe3cf1eb 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -517,7 +517,7 @@ describe Note do describe '#to_ability_name' do it 'returns snippet for a project snippet note' do - expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet') + expect(build(:note_on_project_snippet).to_ability_name).to eq('project_snippet') end it 'returns personal_snippet for a personal snippet note' do diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index e8096358f7d..7e25c53e77c 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -10,11 +10,50 @@ describe NotePolicy, mdoels: true do return @policies if @policies noteable ||= issue - note = create(:note, noteable: noteable, author: user, project: project) + note = if noteable.is_a?(Commit) + create(:note_on_commit, commit_id: noteable.id, author: user, project: project) + else + create(:note, noteable: noteable, author: user, project: project) + end @policies = described_class.new(user, note) end + shared_examples_for 'a discussion with a private noteable' do + let(:noteable) { issue } + let(:policy) { policies(noteable) } + + context 'when the note author can no longer see the noteable' do + it 'can not edit nor read the note' do + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:read_note) + end + end + + context 'when the note author can still see the noteable' do + before do + project.add_developer(user) + end + + it 'can edit the note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) + end + end + end + + context 'when the project is private' do + let(:project) { create(:project, :private, :repository) } + + context 'when the noteable is a commit' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { project.repository.head_commit } + end + end + end + context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do @@ -24,14 +63,48 @@ describe NotePolicy, mdoels: true do end end - context 'when the noteable is a snippet' do + context 'when the noteable is a project snippet' do it 'can edit note' do - policies = policies(create(:project_snippet, project: project)) + policies = policies(create(:project_snippet, :public, project: project)) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) end + + context 'when it is private' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { create(:project_snippet, :private, project: project) } + end + end + end + + context 'when the noteable is a personal snippet' do + it 'can edit note' do + policies = policies(create(:personal_snippet, :public)) + + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + + context 'when it is private' do + it 'can not edit nor read the note' do + policies = policies(create(:personal_snippet, :private)) + + expect(policies).to be_disallowed(:admin_note) + expect(policies).to be_disallowed(:resolve_note) + expect(policies).to be_disallowed(:read_note) + end + end + end + + context 'when a discussion is confidential' do + before do + issue.update_attribute(:confidential, true) + end + + it_behaves_like 'a discussion with a private noteable' end context 'when a discussion is locked' do