From 5e02ef43bc655456ce3004a1f47a6dcf5200c1d2 Mon Sep 17 00:00:00 2001
From: Alex Kalderimis <akalderimis@gitlab.com>
Date: Wed, 29 Sep 2021 14:03:49 +0100
Subject: [PATCH] Add BaseEdge base class

See https://gitlab.com/gitlab-org/gitlab/-/issues/341911

This adds a new base class for edges that uses our custom `BaseField`
as the standard field class.

We do not need to customize authorization for edges, since nodes are
redacted by our connections before edges are constructed from them.

All our edges should inherit from this base class in order to use field
customizations.

Changelog: changed
---
 app/graphql/types/base_edge.rb         |  7 +++
 app/graphql/types/base_field.rb        |  2 +
 rubocop/cop/graphql/authorize_types.rb |  2 +-
 spec/graphql/types/base_edge_spec.rb   | 76 ++++++++++++++++++++++++++
 4 files changed, 86 insertions(+), 1 deletion(-)
 create mode 100644 app/graphql/types/base_edge.rb
 create mode 100644 spec/graphql/types/base_edge_spec.rb

diff --git a/app/graphql/types/base_edge.rb b/app/graphql/types/base_edge.rb
new file mode 100644
index 0000000000000..f4409c983f881
--- /dev/null
+++ b/app/graphql/types/base_edge.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Types
+  class BaseEdge < GraphQL::Types::Relay::BaseEdge
+    field_class Types::BaseField
+  end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 93e17ea6dfcae..75909592c6cb5 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -78,6 +78,8 @@ def visible?(context)
     attr_reader :feature_flag
 
     def field_authorized?(object, ctx)
+      object = object.node if object.is_a?(GraphQL::Pagination::Connection::Edge)
+
       authorization.ok?(object, ctx[:current_user])
     end
 
diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb
index 180a1a27a858c..d5866aa0aaf57 100644
--- a/rubocop/cop/graphql/authorize_types.rb
+++ b/rubocop/cop/graphql/authorize_types.rb
@@ -8,7 +8,7 @@ class AuthorizeTypes < RuboCop::Cop::Cop
               'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
 
         # We want to exclude our own basetypes and scalars
-        ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType SubscriptionType
+        ALLOWED_TYPES = %w[BaseEnum BaseEdge BaseScalar BasePermissionType MutationType SubscriptionType
                            QueryType GraphQL::Schema BaseUnion BaseInputObject].freeze
 
         def_node_search :authorize?, <<~PATTERN
diff --git a/spec/graphql/types/base_edge_spec.rb b/spec/graphql/types/base_edge_spec.rb
new file mode 100644
index 0000000000000..237726a352c40
--- /dev/null
+++ b/spec/graphql/types/base_edge_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::BaseEdge do
+  include GraphqlHelpers
+
+  let_it_be(:test_schema) do
+    project_edge_type = Class.new(described_class) do
+      field :proof_of_admin_rights, String,
+            null: true, authorize: :admin_project
+
+      def proof_of_admin_rights
+        'ok'
+      end
+    end
+
+    project_type = Class.new(::Types::BaseObject) do
+      graphql_name 'Project'
+      authorize :read_project
+      edge_type_class project_edge_type
+
+      field :name, String, null: false
+    end
+
+    Class.new(GraphQL::Schema) do
+      lazy_resolve ::Gitlab::Graphql::Lazy, :force
+      use ::GraphQL::Pagination::Connections
+      use ::Gitlab::Graphql::Pagination::Connections
+
+      query(Class.new(::Types::BaseObject) do
+        graphql_name 'Query'
+        field :projects, project_type.connection_type, null: false
+
+        def projects
+          context[:projects]
+        end
+      end)
+    end
+  end
+
+  def document
+    GraphQL.parse(<<~GQL)
+    query {
+      projects {
+        edges {
+          proofOfAdminRights
+          node { name }
+        }
+      }
+    }
+    GQL
+  end
+
+  it 'supports field authorization on edge fields' do
+    user = create(:user)
+    private_project = create(:project, :private)
+    member_project = create(:project, :private)
+    maintainer_project = create(:project, :private)
+    public_project = create(:project, :public)
+
+    member_project.add_developer(user)
+    maintainer_project.add_maintainer(user)
+    projects = [private_project, member_project, maintainer_project, public_project]
+
+    data = { current_user: user, projects: projects }
+    query = GraphQL::Query.new(test_schema, document: document, context: data)
+    result = query.result.to_h
+
+    expect(graphql_dig_at(result, 'data', 'projects', 'edges', 'node', 'name'))
+      .to contain_exactly(member_project.name, maintainer_project.name, public_project.name)
+
+    expect(graphql_dig_at(result, 'data', 'projects', 'edges', 'proofOfAdminRights'))
+      .to contain_exactly('ok')
+  end
+end
-- 
GitLab