diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index ae600f6af323f13add507c9905ab5766803c8749..bfb406f37a4aead293e3bca34c458514ed7c90e4 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -404,6 +404,7 @@ Gitlab/NamespacedClass:
     - 'app/policies/project_hook_policy.rb'
     - 'app/policies/prometheus_alert_policy.rb'
     - 'app/policies/protected_branch_policy.rb'
+    - 'app/policies/protected_branch_access_policy.rb'
     - 'app/policies/release_policy.rb'
     - 'app/policies/repository_policy.rb'
     - 'app/policies/resource_label_event_policy.rb'
diff --git a/app/graphql/types/branch_protections/base_access_level_type.rb b/app/graphql/types/branch_protections/base_access_level_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2fd9e3942960841911f0d6ba5239fbd5767e1c10
--- /dev/null
+++ b/app/graphql/types/branch_protections/base_access_level_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+  module BranchProtections
+    class BaseAccessLevelType < Types::BaseObject
+      authorize :read_protected_branch
+
+      field :access_level,
+            type: GraphQL::Types::Int,
+            null: false,
+            description: 'GitLab::Access level.'
+
+      field :access_level_description,
+            type: GraphQL::Types::String,
+            null: false,
+            description: 'Human readable representation for this access level.',
+            hash_key: 'humanize'
+    end
+  end
+end
diff --git a/app/graphql/types/branch_protections/merge_access_level_type.rb b/app/graphql/types/branch_protections/merge_access_level_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85295e1ba25d5a9ea486b43903c762eb5b25ac4a
--- /dev/null
+++ b/app/graphql/types/branch_protections/merge_access_level_type.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+  module BranchProtections
+    class MergeAccessLevelType < BaseAccessLevelType # rubocop:disable Graphql/AuthorizeTypes
+      graphql_name 'MergeAccessLevel'
+      description 'Represents the merge access level of a branch protection.'
+      accepts ::ProtectedBranch::MergeAccessLevel
+    end
+  end
+end
diff --git a/app/graphql/types/branch_rules/branch_protection_type.rb b/app/graphql/types/branch_rules/branch_protection_type.rb
index b391eba58beb3e2aacc32c56f36a1b495d1107a6..02379dcf5582ff3ff6cad2f991bafd7268857b59 100644
--- a/app/graphql/types/branch_rules/branch_protection_type.rb
+++ b/app/graphql/types/branch_rules/branch_protection_type.rb
@@ -8,6 +8,11 @@ class BranchProtectionType < BaseObject
       accepts ::ProtectedBranch
       authorize :read_protected_branch
 
+      field :merge_access_levels,
+            type: Types::BranchProtections::MergeAccessLevelType.connection_type,
+            null: true,
+            description: 'Details about who can merge when this branch is the source branch.'
+
       field :allow_force_push,
             type: GraphQL::Types::Boolean,
             null: false,
diff --git a/app/policies/protected_branch_access_policy.rb b/app/policies/protected_branch_access_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f33af89d2a43b4540ddd71ba8600c9a55494b45
--- /dev/null
+++ b/app/policies/protected_branch_access_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProtectedBranchAccessPolicy < BasePolicy
+  delegate { @subject.protected_branch }
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 25fd23ffddb0398887f36722850ba544dbc49fc7..ebe922239c9e5d8f1b94c0d162d921bb14cb18b1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7837,6 +7837,29 @@ The edge type for [`MemberInterface`](#memberinterface).
 | <a id="memberinterfaceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="memberinterfaceedgenode"></a>`node` | [`MemberInterface`](#memberinterface) | The item at the end of the edge. |
 
+#### `MergeAccessLevelConnection`
+
+The connection type for [`MergeAccessLevel`](#mergeaccesslevel).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mergeaccesslevelconnectionedges"></a>`edges` | [`[MergeAccessLevelEdge]`](#mergeaccessleveledge) | A list of edges. |
+| <a id="mergeaccesslevelconnectionnodes"></a>`nodes` | [`[MergeAccessLevel]`](#mergeaccesslevel) | A list of nodes. |
+| <a id="mergeaccesslevelconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `MergeAccessLevelEdge`
+
+The edge type for [`MergeAccessLevel`](#mergeaccesslevel).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mergeaccessleveledgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="mergeaccessleveledgenode"></a>`node` | [`MergeAccessLevel`](#mergeaccesslevel) | The item at the end of the edge. |
+
 #### `MergeRequestAssigneeConnection`
 
 The connection type for [`MergeRequestAssignee`](#mergerequestassignee).
@@ -10083,6 +10106,7 @@ Branch protection details for a branch rule.
 | Name | Type | Description |
 | ---- | ---- | ----------- |
 | <a id="branchprotectionallowforcepush"></a>`allowForcePush` | [`Boolean!`](#boolean) | Toggle force push to the branch for users with write access. |
+| <a id="branchprotectionmergeaccesslevels"></a>`mergeAccessLevels` | [`MergeAccessLevelConnection`](#mergeaccesslevelconnection) | Details about who can merge when this branch is the source branch. (see [Connections](#connections)) |
 
 ### `BranchRule`
 
@@ -13783,6 +13807,17 @@ Maven metadata.
 | <a id="mavenmetadatapath"></a>`path` | [`String!`](#string) | Path of the Maven package. |
 | <a id="mavenmetadataupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
 
+### `MergeAccessLevel`
+
+Represents the merge access level of a branch protection.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mergeaccesslevelaccesslevel"></a>`accessLevel` | [`Int!`](#int) | GitLab::Access level. |
+| <a id="mergeaccesslevelaccessleveldescription"></a>`accessLevelDescription` | [`String!`](#string) | Human readable representation for this access level. |
+
 ### `MergeRequest`
 
 #### Fields
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index cee7b57723611ce167b156471ca23c5ff49c5fe8..142ddb2b2c7548b7b506299a1decc4336e752671 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -17,6 +17,16 @@
       ProtectedBranches::CacheService.new(protected_branch.project).refresh
     end
 
+    after(:build) do |protected_branch, evaluator|
+      if evaluator.default_access_level && evaluator.default_push_level
+        protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+      end
+
+      if evaluator.default_access_level && evaluator.default_merge_level
+        protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+      end
+    end
+
     trait :create_branch_on_repository do
       association :project, factory: [:project, :repository]
 
@@ -31,59 +41,63 @@
       end
     end
 
-    trait :developers_can_push do
+    trait :maintainers_can_push do
       transient do
         default_push_level { false }
       end
 
       after(:build) do |protected_branch|
-        protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+        protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
       end
     end
 
-    trait :developers_can_merge do
+    trait :maintainers_can_merge do
       transient do
-        default_merge_level { false }
+        default_push_level { false }
       end
 
       after(:build) do |protected_branch|
-        protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+        protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
       end
     end
 
-    trait :no_one_can_push do
+    trait :developers_can_push do
       transient do
         default_push_level { false }
       end
 
       after(:build) do |protected_branch|
-        protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
+        protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
       end
     end
 
-    trait :maintainers_can_push do
+    trait :developers_can_merge do
       transient do
-        default_push_level { false }
+        default_merge_level { false }
       end
 
       after(:build) do |protected_branch|
-        protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+        protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
       end
     end
 
-    after(:build) do |protected_branch, evaluator|
-      if evaluator.default_access_level && evaluator.default_push_level
-        protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+    trait :no_one_can_push do
+      transient do
+        default_push_level { false }
       end
 
-      if evaluator.default_access_level && evaluator.default_merge_level
-        protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+      after(:build) do |protected_branch|
+        protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
       end
     end
 
     trait :no_one_can_merge do
-      after(:create) do |protected_branch|
-        protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
+      transient do
+        default_merge_level { false }
+      end
+
+      after(:build) do |protected_branch|
+        protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
       end
     end
   end
diff --git a/spec/graphql/types/branch_protection_type_spec.rb b/spec/graphql/types/branch_protection_type_spec.rb
index 6efa457227f954e101e2cfcde5752eda1074c211..5891df666c5e536a6811f4bc125132114d089020 100644
--- a/spec/graphql/types/branch_protection_type_spec.rb
+++ b/spec/graphql/types/branch_protection_type_spec.rb
@@ -5,7 +5,7 @@
 RSpec.describe GitlabSchema.types['BranchProtection'] do
   subject { described_class }
 
-  let(:fields) { %i[allow_force_push] }
+  let(:fields) { %i[merge_access_levels allow_force_push] }
 
   specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
 
diff --git a/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb b/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c9c8553b6f92e055c55e0934e0e4c10959a4532
--- /dev/null
+++ b/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeAccessLevel'] do
+  subject { described_class }
+
+  let(:fields) { %i[access_level access_level_description] }
+
+  specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+
+  specify { is_expected.to have_graphql_fields(fields) }
+end
diff --git a/spec/policies/protected_branch_access_policy_spec.rb b/spec/policies/protected_branch_access_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..68a130d666a7448d44b721e8a94d425e485af296
--- /dev/null
+++ b/spec/policies/protected_branch_access_policy_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedBranchAccessPolicy do
+  let(:user) { create(:user) }
+  let(:protected_branch_access) { create(:protected_branch_merge_access_level) }
+  let(:project) { protected_branch_access.protected_branch.project }
+
+  subject { described_class.new(user, protected_branch_access) }
+
+  context 'as maintainers' do
+    before do
+      project.add_maintainer(user)
+    end
+
+    it 'can be read' do
+      is_expected.to be_allowed(:read_protected_branch)
+    end
+  end
+
+  context 'as guests' do
+    before do
+      project.add_guest(user)
+    end
+
+    it 'can not be read' do
+      is_expected.to be_disallowed(:read_protected_branch)
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb5006ec8e4d5d1b3c57d2f8098472bd4814d93f
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting merge access levels for a branch protection' do
+  include GraphqlHelpers
+
+  let_it_be(:current_user) { create(:user) }
+
+  let(:merge_access_level_data) { merge_access_levels_data[0] }
+
+  let(:merge_access_levels_data) do
+    graphql_data_at('project',
+                    'branchRules',
+                    'nodes',
+                    0,
+                    'branchProtection',
+                    'mergeAccessLevels',
+                    'nodes')
+  end
+
+  let(:project) { protected_branch.project }
+
+  let(:merge_access_levels_count) { protected_branch.merge_access_levels.size }
+
+  let(:variables) { { path: project.full_path } }
+
+  let(:fields) { all_graphql_fields_for('MergeAccessLevel') }
+
+  let(:query) do
+    <<~GQL
+    query($path: ID!) {
+      project(fullPath: $path) {
+        branchRules(first: 1) {
+          nodes {
+            branchProtection {
+              mergeAccessLevels {
+                nodes {
+                  #{fields}
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    GQL
+  end
+
+  context 'when the user does not have read_protected_branch abilities' do
+    let_it_be(:protected_branch) { create(:protected_branch) }
+
+    before do
+      project.add_guest(current_user)
+      post_graphql(query, current_user: current_user, variables: variables)
+    end
+
+    it_behaves_like 'a working graphql query'
+
+    it { expect(merge_access_levels_data).not_to be_present }
+  end
+
+  shared_examples 'merge access request' do
+    let(:merge_access) { protected_branch.merge_access_levels.first }
+
+    before do
+      project.add_maintainer(current_user)
+      post_graphql(query, current_user: current_user, variables: variables)
+    end
+
+    it_behaves_like 'a working graphql query'
+
+    it 'returns all merge access levels' do
+      expect(merge_access_levels_data.size).to eq(merge_access_levels_count)
+    end
+
+    it 'includes access_level' do
+      expect(merge_access_level_data['accessLevel'])
+        .to eq(merge_access.access_level)
+    end
+
+    it 'includes access_level_description' do
+      expect(merge_access_level_data['accessLevelDescription'])
+        .to eq(merge_access.humanize)
+    end
+  end
+
+  context 'when the user does have read_protected_branch abilities' do
+    let(:merge_access) { protected_branch.merge_access_levels.first }
+
+    context 'when no one has access' do
+      let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) }
+
+      it_behaves_like 'merge access request'
+    end
+
+    context 'when developers have access' do
+      let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) }
+
+      it_behaves_like 'merge access request'
+    end
+
+    context 'when maintainers have access' do
+      let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) }
+
+      it_behaves_like 'merge access request'
+    end
+  end
+end