summaryrefslogtreecommitdiff
path: root/lib/gitlab/graphql/authorize/authorize_field_service.rb
blob: e8db619f88ab995796779ec9c51f7e30885042f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# frozen_string_literal: true

module Gitlab
  module Graphql
    module Authorize
      class AuthorizeFieldService
        def initialize(field)
          @field = field
          @old_resolve_proc = @field.resolve_proc
        end

        def authorizations?
          authorizations.present?
        end

        def authorized_resolve
          proc do |parent_typed_object, args, ctx|
            resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
            authorizing_object = authorize_against(parent_typed_object, resolved_type)

            filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
          end
        end

        private

        def authorizations
          @authorizations ||= (type_authorizations + field_authorizations).uniq
        end

        # Returns any authorize metadata from the return type of @field
        def type_authorizations
          type = @field.type

          # When the return type of @field is a collection, find the singular type
          if @field.connection?
            type = node_type_for_relay_connection(type)
          elsif type.list?
            type = node_type_for_basic_connection(type)
          end

          type = type.unwrap if type.kind.non_null?

          Array.wrap(type.metadata[:authorize])
        end

        # Returns any authorize metadata from @field
        def field_authorizations
          return [] if @field.metadata[:authorize] == true

          Array.wrap(@field.metadata[:authorize])
        end

        def authorize_against(parent_typed_object, resolved_type)
          if scalar_type?
            # The field is a built-in/scalar type, or a list of scalars
            # authorize using the parent's object
            parent_typed_object.object
          elsif @field.connection? || @field.type.list? || resolved_type.is_a?(Array)
            # The field is a connection or a list of non-built-in types, we'll
            # authorize each element when rendering
            nil
          elsif resolved_type.respond_to?(:object)
            # The field is a type representing a single object, we'll authorize
            # against the object directly
            resolved_type.object
          else
            # Resolved type is a single object that might not be loaded yet by
            # the batchloader, we'll authorize that
            resolved_type
          end
        end

        def filter_allowed(current_user, resolved_type, authorizing_object)
          if resolved_type.nil?
            # We're not rendering anything, for example when a record was not found
            # no need to do anything
          elsif authorizing_object
            # Authorizing fields representing scalars, or a simple field with an object
            ::Gitlab::Graphql::Lazy.with_value(authorizing_object) do |object|
              resolved_type if allowed_access?(current_user, object)
            end
          elsif @field.connection?
            ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |type|
              # A connection with pagination, modify the visible nodes on the
              # connection type in place
              nodes = to_nodes(type)
              nodes.keep_if { |node| allowed_access?(current_user, node) } if nodes
              type
            end
          elsif @field.type.list? || resolved_type.is_a?(Array)
            # A simple list of rendered types  each object being an object to authorize
            ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |items|
              items.select do |single_object_type|
                object_type = realized(single_object_type)
                object = object_type.try(:object) || object_type
                allowed_access?(current_user, object)
              end
            end
          else
            raise "Can't authorize #{@field}"
          end
        end

        # Ensure that we are dealing with realized objects, not delayed promises
        def realized(thing)
          ::Gitlab::Graphql::Lazy.force(thing)
        end

        # Try to get the connection
        # can be at type.object or at type
        def to_nodes(type)
          if type.respond_to?(:nodes)
            type.nodes
          elsif type.respond_to?(:object)
            to_nodes(type.object)
          else
            nil
          end
        end

        def allowed_access?(current_user, object)
          object = realized(object)

          authorizations.all? do |ability|
            Ability.allowed?(current_user, ability, object)
          end
        end

        # Returns the singular type for relay connections.
        # This will be the type class of edges.node
        def node_type_for_relay_connection(type)
          type.unwrap.get_field('edges').type.unwrap.get_field('node').type
        end

        # Returns the singular type for basic connections, for example `[Types::ProjectType]`
        def node_type_for_basic_connection(type)
          type.unwrap
        end

        def scalar_type?
          node_type_for_basic_connection(@field.type).kind.scalar?
        end
      end
    end
  end
end