From 23ffc95c794126ae4039ab6a209f90c6df03d14a Mon Sep 17 00:00:00 2001
From: Valerie Burton <vburton@gitlab.com>
Date: Tue, 9 Aug 2022 12:27:04 -0500
Subject: [PATCH] Inherited permissions tests for merge requests

Refactors and adds new specs to test inherited permissions for MRs
---
 .../user_creates_merge_request_spec.rb        | 183 ++++++---
 .../mutations/merge_requests/create_spec.rb   | 119 ++++--
 spec/policies/merge_request_policy_spec.rb    | 374 ++++++++++++------
 spec/requests/api/merge_requests_spec.rb      |  91 +++++
 4 files changed, 537 insertions(+), 230 deletions(-)

diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index c8b22bb31253f..0ae4ef186491d 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -1,111 +1,164 @@
 # frozen_string_literal: true
 
-require "spec_helper"
+require 'spec_helper'
 
-RSpec.describe "User creates a merge request", :js do
+RSpec.describe 'User creates a merge request', :js do
   include ProjectForksHelper
 
-  let_it_be(:project) { create(:project, :repository) }
-  let_it_be(:user) { create(:user) }
-
-  let(:title) { "Some feature" }
-
-  before do
-    project.add_maintainer(user)
-    sign_in(user)
-  end
-
-  it "creates a merge request" do
-    visit(project_new_merge_request_path(project))
+  shared_examples 'creates a merge request' do
+    specify do
+      visit(project_new_merge_request_path(project))
 
-    find(".js-source-branch").click
-    click_link("fix")
+      compare_source_and_target('fix', 'feature')
 
-    find(".js-target-branch").click
-    click_link("feature")
+      page.within('.merge-request-form') do
+        expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
+      end
 
-    click_button("Compare branches")
+      fill_in('Title', with: title)
+      click_button('Create merge request')
 
-    page.within('.merge-request-form') do
-      expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
+      page.within('.merge-request') do
+        expect(page).to have_content(title)
+      end
     end
+  end
 
-    fill_in("Title", with: title)
-    click_button("Create merge request")
+  shared_examples 'renders not found' do
+    specify do
+      visit project_new_merge_request_path(project)
 
-    page.within(".merge-request") do
-      expect(page).to have_content(title)
+      expect(page).to have_title('Not Found')
+      expect(page).to have_content('Page Not Found')
     end
   end
 
-  context "XSS branch name exists" do
+  context 'when user is a direct project member' do
+    let_it_be(:project) { create(:project, :repository) }
+    let_it_be(:user) { create(:user) }
+
+    let(:title) { 'Some feature' }
+
     before do
-      project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master")
+      project.add_maintainer(user)
+      sign_in(user)
     end
 
-    it "doesn't execute the dodgy branch name" do
-      visit(project_new_merge_request_path(project))
+    it_behaves_like 'creates a merge request'
 
-      find(".js-source-branch").click
-      click_link("<img/src='x'/onerror=alert('oops')>")
+    context 'with XSS branch name' do
+      before do
+        project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", 'master')
+      end
 
-      find(".js-target-branch").click
-      click_link("feature")
+      it 'does not execute the suspicious branch name' do
+        visit(project_new_merge_request_path(project))
 
-      click_button("Compare branches")
+        compare_source_and_target("<img/src='x'/onerror=alert('oops')>", 'feature')
 
-      expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
+        expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
+      end
     end
-  end
 
-  context "to a forked project" do
-    let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+    context 'to a forked project' do
+      let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+
+      it 'creates a merge request', :sidekiq_might_not_need_inline do
+        visit(project_new_merge_request_path(forked_project))
+
+        expect(page).to have_content('Source branch').and have_content('Target branch')
+        expect(find('#merge_request_target_project_id', visible: false).value).to eq(project.id.to_s)
 
-    it "creates a merge request", :sidekiq_might_not_need_inline do
-      visit(project_new_merge_request_path(forked_project))
+        click_button('Compare branches and continue')
 
-      expect(page).to have_content("Source branch").and have_content("Target branch")
-      expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s)
+        expect(page).to have_content('You must select source and target branch')
 
-      click_button("Compare branches and continue")
+        first('.js-source-project').click
+        first('.dropdown-source-project a', text: forked_project.full_path)
 
-      expect(page).to have_content("You must select source and target branch")
+        first('.js-target-project').click
+        first('.dropdown-target-project a', text: project.full_path)
 
-      first(".js-source-project").click
-      first(".dropdown-source-project a", text: forked_project.full_path)
+        first('.js-source-branch').click
 
-      first(".js-target-project").click
-      first(".dropdown-target-project a", text: project.full_path)
+        wait_for_requests
 
-      first(".js-source-branch").click
+        source_branch = 'fix'
 
-      wait_for_requests
+        first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click
 
-      source_branch = "fix"
+        click_button('Compare branches and continue')
 
-      first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click
+        expect(page).to have_text _('New merge request')
 
-      click_button("Compare branches and continue")
+        page.within('form#new_merge_request') do
+          fill_in('Title', with: title)
+        end
 
-      expect(page).to have_text _('New merge request')
+        expect(find('.js-assignee-search')['data-project-id']).to eq(project.id.to_s)
+        find('.js-assignee-search').click
 
-      page.within("form#new_merge_request") do
-        fill_in("Title", with: title)
+        page.within('.dropdown-menu-user') do
+          expect(page).to have_content('Unassigned')
+                      .and have_content(user.name)
+                      .and have_content(project.users.first.name)
+        end
+        find('.js-assignee-search').click
+
+        click_button('Create merge request')
+
+        expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
+      end
+    end
+  end
+
+  context 'when user is an inherited member from the group' do
+    let_it_be(:group) { create(:group, :public) }
+
+    let(:user) { create(:user) }
+
+    context 'when project is public and merge requests are private' do
+      let_it_be(:project) do
+        create(:project,
+               :public,
+               :repository,
+               group: group,
+               merge_requests_access_level: ProjectFeature::DISABLED)
       end
 
-      expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
-      find('.js-assignee-search').click
+      context 'and user is a guest' do
+        before do
+          group.add_guest(user)
+          sign_in(user)
+        end
 
-      page.within(".dropdown-menu-user") do
-        expect(page).to have_content("Unassigned")
-                   .and have_content(user.name)
-                   .and have_content(project.users.first.name)
+        it_behaves_like 'renders not found'
       end
-      find('.js-assignee-search').click
+    end
+
+    context 'when project is private' do
+      let_it_be(:project) { create(:project, :private, :repository, group: group) }
 
-      click_button("Create merge request")
+      context 'and user is a guest' do
+        before do
+          group.add_guest(user)
+          sign_in(user)
+        end
 
-      expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
+        it_behaves_like 'renders not found'
+      end
     end
   end
+
+  private
+
+  def compare_source_and_target(source_branch, target_branch)
+    find('.js-source-branch').click
+    click_link(source_branch)
+
+    find('.js-target-branch').click
+    click_link(target_branch)
+
+    click_button('Compare branches')
+  end
 end
diff --git a/spec/graphql/mutations/merge_requests/create_spec.rb b/spec/graphql/mutations/merge_requests/create_spec.rb
index e1edb60e4ff96..6e593a5f4bef0 100644
--- a/spec/graphql/mutations/merge_requests/create_spec.rb
+++ b/spec/graphql/mutations/merge_requests/create_spec.rb
@@ -7,8 +7,7 @@
 
   subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
 
-  let_it_be(:project) { create(:project, :public, :repository) }
-  let_it_be(:user) { create(:user) }
+  let(:user) { create(:user) }
 
   let(:context) do
     GraphQL::Query::Context.new(
@@ -38,62 +37,106 @@
 
     let(:mutated_merge_request) { subject[:merge_request] }
 
-    it 'raises an error if the resource is not accessible to the user' do
-      expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+    shared_examples 'resource not available' do
+      it 'raises an error' do
+        expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+      end
     end
 
-    context 'when user does not have enough permissions to create a merge request' do
-      before do
-        project.add_guest(user)
-      end
+    context 'when user is not a project member' do
+      let_it_be(:project) { create(:project, :public, :repository) }
 
-      it 'raises an error if the resource is not accessible to the user' do
-        expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
-      end
+      it_behaves_like 'resource not available'
     end
 
-    context 'when the user can create a merge request' do
-      before_all do
-        project.add_developer(user)
-      end
+    context 'when user is a direct project member' do
+      let_it_be(:project) { create(:project, :public, :repository) }
 
-      it 'creates a new merge request' do
-        expect { mutated_merge_request }.to change(MergeRequest, :count).by(1)
-      end
+      context 'and user is a guest' do
+        before do
+          project.add_guest(user)
+        end
 
-      it 'returns a new merge request' do
-        expect(mutated_merge_request.title).to eq(title)
-        expect(subject[:errors]).to be_empty
+        it_behaves_like 'resource not available'
       end
 
-      context 'when optional description field is set' do
-        let(:description) { 'content' }
+      context 'and user is a developer' do
+        before do
+          project.add_developer(user)
+        end
+
+        it 'creates a new merge request' do
+          expect { mutated_merge_request }.to change(MergeRequest, :count).by(1)
+        end
 
-        it 'returns a new merge request with a description' do
-          expect(mutated_merge_request.description).to eq(description)
+        it 'returns a new merge request' do
+          expect(mutated_merge_request.title).to eq(title)
           expect(subject[:errors]).to be_empty
         end
-      end
 
-      context 'when optional labels field is set' do
-        let(:labels) { %w[label-1 label-2] }
+        context 'when optional description field is set' do
+          let(:description) { 'content' }
 
-        it 'returns a new merge request with labels' do
-          expect(mutated_merge_request.labels.map(&:title)).to eq(labels)
-          expect(subject[:errors]).to be_empty
+          it 'returns a new merge request with a description' do
+            expect(mutated_merge_request.description).to eq(description)
+            expect(subject[:errors]).to be_empty
+          end
+        end
+
+        context 'when optional labels field is set' do
+          let(:labels) { %w[label-1 label-2] }
+
+          it 'returns a new merge request with labels' do
+            expect(mutated_merge_request.labels.map(&:title)).to eq(labels)
+            expect(subject[:errors]).to be_empty
+          end
+        end
+
+        context 'when service cannot create a merge request' do
+          let(:title) { nil }
+
+          it 'does not create a new merge request' do
+            expect { mutated_merge_request }.not_to change(MergeRequest, :count)
+          end
+
+          it 'returns errors' do
+            expect(mutated_merge_request).to be_nil
+            expect(subject[:errors]).to match_array(['Title can\'t be blank'])
+          end
         end
       end
+    end
 
-      context 'when service cannot create a merge request' do
-        let(:title) { nil }
+    context 'when user is an inherited member from the group' do
+      let_it_be(:group) { create(:group, :public) }
 
-        it 'does not create a new merge request' do
-          expect { mutated_merge_request }.not_to change(MergeRequest, :count)
+      context 'when project is public with private merge requests' do
+        let_it_be(:project) do
+          create(:project,
+                 :public,
+                 :repository,
+                 group: group,
+                 merge_requests_access_level: ProjectFeature::DISABLED)
         end
 
-        it 'returns errors' do
-          expect(mutated_merge_request).to be_nil
-          expect(subject[:errors]).to eq(['Title can\'t be blank'])
+        context 'and user is a guest' do
+          before do
+            group.add_guest(user)
+          end
+
+          it_behaves_like 'resource not available'
+        end
+      end
+
+      context 'when project is private' do
+        let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+        context 'and user is a guest' do
+          before do
+            group.add_guest(user)
+          end
+
+          it_behaves_like 'resource not available'
         end
       end
     end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index dd42e1b931343..7e1af132b1d14 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -7,24 +7,18 @@
 
   let_it_be(:guest) { create(:user) }
   let_it_be(:author) { create(:user) }
+  let_it_be(:reporter) { create(:user) }
   let_it_be(:developer) { create(:user) }
   let_it_be(:non_team_member) { create(:user) }
 
-  let(:project) { create(:project, :public) }
-
   def permissions(user, merge_request)
     described_class.new(user, merge_request)
   end
 
-  before do
-    project.add_guest(guest)
-    project.add_guest(author)
-    project.add_developer(developer)
-  end
-
   mr_perms = %i[create_merge_request_in
                 create_merge_request_from
                 read_merge_request
+                update_merge_request
                 create_todo
                 approve_merge_request
                 create_note
@@ -40,7 +34,28 @@ def permissions(user, merge_request)
     end
   end
 
-  shared_examples_for 'a user with access' do
+  shared_examples_for 'a user with reporter access' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:policy, :is_allowed) do
+      :create_merge_request_in   | true
+      :read_merge_request        | true
+      :create_todo               | true
+      :create_note               | true
+      :update_subscription       | true
+      :create_merge_request_from | false
+      :approve_merge_request     | false
+      :update_merge_request      | false
+    end
+
+    with_them do
+      specify do
+        is_allowed ? (is_expected.to be_allowed(policy)) : (is_expected.to be_disallowed(policy))
+      end
+    end
+  end
+
+  shared_examples_for 'a user with full access' do
     let(:perms) { permissions(subject, merge_request) }
 
     mr_perms.each do |thing|
@@ -50,199 +65,304 @@ def permissions(user, merge_request)
     end
   end
 
-  context 'when merge request is public' do
-    let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
-    let(:user) { author }
-
-    context 'and user is anonymous' do
-      subject { permissions(nil, merge_request) }
+  context 'when user is a direct project member' do
+    let(:project) { create(:project, :public) }
 
-      it do
-        is_expected.to be_disallowed(:create_todo, :update_subscription)
-      end
+    before do
+      project.add_guest(guest)
+      project.add_guest(author)
+      project.add_developer(developer)
     end
 
-    context 'and user is author' do
-      subject { permissions(user, merge_request) }
+    context 'when merge request is public' do
+      let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+      let(:user) { author }
 
-      context 'and the user is a guest' do
-        let(:user) { guest }
+      context 'and user is author' do
+        subject { permissions(user, merge_request) }
 
-        it do
-          is_expected.to be_allowed(:update_merge_request)
-        end
+        context 'and the user is a guest' do
+          let(:user) { guest }
 
-        it do
-          is_expected.to be_allowed(:reopen_merge_request)
-        end
+          it do
+            is_expected.to be_allowed(:update_merge_request)
+          end
 
-        it do
-          is_expected.to be_allowed(:approve_merge_request)
+          it do
+            is_expected.to be_allowed(:reopen_merge_request)
+          end
+
+          it do
+            is_expected.to be_allowed(:approve_merge_request)
+          end
         end
       end
+    end
 
-      context 'and the user is a group member' do
-        let(:project) { create(:project, :public, group: group) }
-        let(:group) { create(:group) }
-        let(:user) { non_team_member }
-
-        before do
-          group.add_guest(non_team_member)
-        end
+    context 'when merge requests have been disabled' do
+      let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
 
-        it do
-          is_expected.to be_allowed(:approve_merge_request)
-        end
+      before do
+        project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
       end
 
-      context 'and the user is a member of a shared group' do
-        let(:user) { non_team_member }
+      describe 'the author' do
+        subject { author }
 
-        before do
-          group = create(:group)
-          project.project_group_links.create!(
-            group: group,
-            group_access: Gitlab::Access::DEVELOPER)
+        it_behaves_like 'a denied user'
+      end
 
-          group.add_guest(non_team_member)
-        end
+      describe 'a guest' do
+        subject { guest }
 
-        it do
-          is_expected.to be_allowed(:approve_merge_request)
-        end
+        it_behaves_like 'a denied user'
       end
 
-      context 'and the user is not a project member' do
-        let(:user) { non_team_member }
+      describe 'a developer' do
+        subject { developer }
 
-        it do
-          is_expected.not_to be_allowed(:approve_merge_request)
-        end
+        it_behaves_like 'a denied user'
       end
     end
-  end
 
-  context 'when merge requests have been disabled' do
-    let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+    context 'when merge requests are private' do
+      let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
 
-    before do
-      project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
-    end
+      before do
+        project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+        project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+      end
 
-    describe 'the author' do
-      subject { author }
+      describe 'the author' do
+        subject { author }
 
-      it_behaves_like 'a denied user'
+        it_behaves_like 'a denied user'
+      end
+
+      describe 'a developer' do
+        subject { developer }
+
+        it_behaves_like 'a user with full access'
+      end
     end
 
-    describe 'a guest' do
-      subject { guest }
+    context 'when merge request is unlocked' do
+      let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) }
 
-      it_behaves_like 'a denied user'
+      it 'allows author to reopen merge request' do
+        expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request)
+      end
+
+      it 'allows developer to reopen merge request' do
+        expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request)
+      end
+
+      it 'prevents guest from reopening merge request' do
+        expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request)
+      end
     end
 
-    describe 'a developer' do
-      subject { developer }
+    context 'when merge request is locked' do
+      let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) }
 
-      it_behaves_like 'a denied user'
+      it 'prevents author from reopening merge request' do
+        expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+      end
+
+      it 'prevents developer from reopening merge request' do
+        expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+      end
+
+      it 'prevents guests from reopening merge request' do
+        expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+      end
+
+      context 'when the user is project member, with at least guest access' do
+        let(:user) { guest }
+
+        it 'can create a note' do
+          expect(permissions(user, merge_request_locked)).to be_allowed(:create_note)
+        end
+      end
     end
 
-    describe 'any other user' do
-      subject { non_team_member }
+    context 'with external authorization enabled' do
+      let(:user) { create(:user) }
+      let(:project) { create(:project, :public) }
+      let(:merge_request) { create(:merge_request, source_project: project) }
+      let(:policies) { described_class.new(user, merge_request) }
 
-      it_behaves_like 'a denied user'
+      before do
+        enable_external_authorization_service_check
+      end
+
+      it 'can read the issue iid without accessing the external service' do
+        expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+        expect(policies).to be_allowed(:read_merge_request_iid)
+      end
     end
   end
 
-  context 'when merge requests are private' do
-    let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+  context 'when user is an inherited member from the parent group' do
+    let_it_be(:group) { create(:group, :public) }
 
-    before do
-      project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
-      project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+    let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+
+    before_all do
+      group.add_guest(guest)
+      group.add_guest(author)
+      group.add_reporter(reporter)
+      group.add_developer(developer)
     end
 
-    describe 'a non-team-member' do
-      subject { non_team_member }
+    context 'when project is public' do
+      let(:project) { create(:project, :public, group: group) }
 
-      it_behaves_like 'a denied user'
-    end
+      describe 'the merge request author' do
+        subject { permissions(author, merge_request) }
 
-    describe 'the author' do
-      subject { author }
+        specify do
+          is_expected.to be_allowed(:approve_merge_request)
+        end
+      end
 
-      it_behaves_like 'a denied user'
+      context 'and merge requests are private' do
+        before do
+          project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+          project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+        end
+
+        describe 'a guest' do
+          subject { guest }
+
+          it_behaves_like 'a denied user'
+        end
+
+        describe 'a reporter' do
+          subject { permissions(reporter, merge_request) }
+
+          it_behaves_like 'a user with reporter access'
+        end
+
+        describe 'a developer' do
+          subject { developer }
+
+          it_behaves_like 'a user with full access'
+        end
+      end
     end
 
-    describe 'a developer' do
-      subject { developer }
+    context 'when project is private' do
+      let(:project) { create(:project, :private, group: group) }
+
+      describe 'a guest' do
+        subject { guest }
 
-      it_behaves_like 'a user with access'
+        it_behaves_like 'a denied user'
+      end
+
+      describe 'a reporter' do
+        subject { permissions(reporter, merge_request) }
+
+        it_behaves_like 'a user with reporter access'
+      end
+
+      describe 'a developer' do
+        subject { developer }
+
+        it_behaves_like 'a user with full access'
+      end
     end
   end
 
-  context 'when merge request is unlocked' do
-    let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) }
+  context 'when user is an inherited member from a shared group' do
+    let(:project) { create(:project, :public) }
+    let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+    let(:user) { author }
 
-    it 'allows author to reopen merge request' do
-      expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request)
+    before do
+      project.add_guest(author)
     end
 
-    it 'allows developer to reopen merge request' do
-      expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request)
-    end
+    context 'and group is given developer access' do
+      let(:user) { non_team_member }
+
+      subject { permissions(user, merge_request) }
+
+      before do
+        group = create(:group)
+        project.project_group_links.create!(
+          group: group,
+          group_access: Gitlab::Access::DEVELOPER)
+
+        group.add_guest(non_team_member)
+      end
 
-    it 'prevents guest from reopening merge request' do
-      expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request)
+      specify do
+        is_expected.to be_allowed(:approve_merge_request)
+      end
     end
   end
 
-  context 'when merge request is locked' do
-    let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) }
+  context 'when user is not a project member' do
+    let(:project) { create(:project, :public) }
 
-    it 'prevents author from reopening merge request' do
-      expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request)
-    end
+    context 'when merge request is public' do
+      let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
 
-    it 'prevents developer from reopening merge request' do
-      expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+      subject { permissions(non_team_member, merge_request) }
+
+      specify do
+        is_expected.not_to be_allowed(:approve_merge_request)
+      end
     end
 
-    it 'prevents guests from reopening merge request' do
-      expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+    context 'when merge requests are disabled' do
+      let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+      before do
+        project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+      end
+
+      subject { non_team_member }
+
+      it_behaves_like 'a denied user'
     end
 
-    context 'when the user is not a project member' do
-      let(:user) { create(:user) }
+    context 'when merge requests are private' do
+      let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
 
-      it 'cannot create a note' do
-        expect(permissions(user, merge_request_locked)).to be_disallowed(:create_note)
+      before do
+        project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+        project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
       end
+
+      subject { non_team_member }
+
+      it_behaves_like 'a denied user'
     end
 
-    context 'when the user is project member, with at least guest access' do
-      let(:user) { guest }
+    context 'when merge request is locked' do
+      let(:merge_request) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project) }
 
-      it 'can create a note' do
-        expect(permissions(user, merge_request_locked)).to be_allowed(:create_note)
+      it 'cannot create a note' do
+        expect(permissions(non_team_member, merge_request)).to be_disallowed(:create_note)
       end
     end
   end
 
-  context 'with external authorization enabled' do
-    let(:user) { create(:user) }
+  context 'when user is anonymous' do
     let(:project) { create(:project, :public) }
-    let(:merge_request) { create(:merge_request, source_project: project) }
-    let(:policies) { described_class.new(user, merge_request) }
 
-    before do
-      enable_external_authorization_service_check
-    end
+    context 'when merge request is public' do
+      let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
 
-    it 'can read the issue iid without accessing the external service' do
-      expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+      subject { permissions(nil, merge_request) }
 
-      expect(policies).to be_allowed(:read_merge_request_iid)
+      specify do
+        is_expected.to be_disallowed(:create_todo, :update_subscription)
+      end
     end
   end
 end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2a03ae8938998..320e9e0cd6665 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1120,6 +1120,44 @@
         end.not_to exceed_query_limit(control)
       end
     end
+
+    context 'when user is an inherited member from the group' do
+      let_it_be(:group) { create(:group) }
+
+      shared_examples 'user cannot view merge requests' do
+        it 'returns 403 forbidden' do
+          get api("/projects/#{group_project.id}/merge_requests", inherited_user)
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'and user is a guest' do
+        let_it_be(:inherited_user) { create(:user) }
+
+        before_all do
+          group.add_guest(inherited_user)
+        end
+
+        context 'when project is public with private merge requests' do
+          let(:group_project) do
+            create(:project,
+                   :public,
+                   :repository,
+                   group: group,
+                   merge_requests_access_level: ProjectFeature::DISABLED)
+          end
+
+          it_behaves_like 'user cannot view merge requests'
+        end
+
+        context 'when project is private' do
+          let(:group_project) { create(:project, :private, :repository, group: group) }
+
+          it_behaves_like 'user cannot view merge requests'
+        end
+      end
+    end
   end
 
   describe "GET /groups/:id/merge_requests" do
@@ -2219,6 +2257,59 @@
         expect(response).to have_gitlab_http_status(:created)
       end
     end
+
+    context 'when user is an inherited member from the group' do
+      let_it_be(:group) { create(:group) }
+
+      shared_examples 'user cannot create merge requests' do
+        it 'returns 403 forbidden' do
+          post api("/projects/#{group_project.id}/merge_requests", inherited_user), params: params
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'and user is a guest' do
+        let_it_be(:inherited_user) { create(:user) }
+        let_it_be(:params) do
+          {
+            title: 'Test merge request',
+            source_branch: 'feature_conflict',
+            target_branch: 'master',
+            author_id: inherited_user.id
+          }
+        end
+
+        before_all do
+          group.add_guest(inherited_user)
+        end
+
+        context 'when project is public with private merge requests' do
+          let(:group_project) do
+            create(:project,
+                   :public,
+                   :repository,
+                   group: group,
+                   merge_requests_access_level: ProjectFeature::DISABLED,
+                   only_allow_merge_if_pipeline_succeeds: false)
+          end
+
+          it_behaves_like 'user cannot create merge requests'
+        end
+
+        context 'when project is private' do
+          let(:group_project) do
+            create(:project,
+                   :private,
+                   :repository,
+                   group: group,
+                   only_allow_merge_if_pipeline_succeeds: false)
+          end
+
+          it_behaves_like 'user cannot create merge requests'
+        end
+      end
+    end
   end
 
   describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do
-- 
GitLab