Welcome to mirror list, hosted at ThFree Co, Russian Federation.

authorize_field_service_spec.rb « authorize « graphql « gitlab « lib « spec - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: c88506899cd6ed0e27333dafa5480ec10ee3e098 (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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# frozen_string_literal: true

require 'spec_helper'

# Also see spec/graphql/features/authorization_spec.rb for
# integration tests of AuthorizeFieldService
RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
  def type(type_authorizations = [])
    Class.new(Types::BaseObject) do
      graphql_name 'TestType'

      authorize type_authorizations
    end
  end

  def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options)
    Class.new(Types::BaseObject) do
      graphql_name 'TestTypeWithField'
      options.reverse_merge!(null: true)
      field :test_field, field_type,
            authorize: field_authorizations,
            **options

      define_method :test_field do
        resolved_value
      end
    end
  end

  def resolve
    service.authorized_resolve[type_instance, {}, context]
  end

  subject(:service) { described_class.new(field) }

  describe '#authorized_resolve' do
    let_it_be(:current_user) { build(:user) }
    let_it_be(:presented_object) { 'presented object' }
    let_it_be(:query_type) { GraphQL::ObjectType.new }
    let_it_be(:schema) { GitlabSchema }
    let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) }
    let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) }

    let(:type_class) { type_with_field(custom_type, :read_field, presented_object) }
    let(:type_instance) { type_class.authorized_new(presented_object, context) }
    let(:field) { type_class.fields['testField'].to_graphql }

    subject(:resolved) { ::Gitlab::Graphql::Lazy.force(resolve) }

    context 'reading the field of a lazy value' do
      let(:ability) { :read_field }
      let(:presented_object) { lazy_upcase('a') }
      let(:type_class) { type_with_field(GraphQL::STRING_TYPE, ability) }

      let(:upcaser) do
        Module.new do
          def self.upcase(strs)
            strs.map(&:upcase)
          end
        end
      end

      def lazy_upcase(str)
        ::BatchLoader::GraphQL.for(str).batch do |strs, found|
          strs.zip(upcaser.upcase(strs)).each { |s, us| found[s, us] }
        end
      end

      it 'does not run authorizations until we force the resolved value' do
        expect(Ability).not_to receive(:allowed?)

        expect(resolve).to respond_to(:force)
      end

      it 'runs authorizations when we force the resolved value' do
        spy_ability_check_for(ability, 'A')

        expect(resolved).to eq('Resolved value')
      end

      it 'redacts values that fail the permissions check' do
        spy_ability_check_for(ability, 'A', passed: false)

        expect(resolved).to be_nil
      end

      context 'we batch two calls' do
        def resolve(value)
          instance = type_class.authorized_new(lazy_upcase(value), context)
          service.authorized_resolve[instance, {}, context]
        end

        it 'batches resolution, but authorizes each object separately' do
          expect(upcaser).to receive(:upcase).once.and_call_original
          spy_ability_check_for(:read_field, 'A', passed: true)
          spy_ability_check_for(:read_field, 'B', passed: false)
          spy_ability_check_for(:read_field, 'C', passed: true)

          a = resolve('a')
          b = resolve('b')
          c = resolve('c')

          expect(a.force).to be_present
          expect(b.force).to be_nil
          expect(c.force).to be_present
        end
      end
    end

    shared_examples 'authorizing fields' do
      context 'scalar types' do
        shared_examples 'checking permissions on the presented object' do
          it 'checks the abilities on the object being presented and returns the value' do
            expected_permissions.each do |permission|
              spy_ability_check_for(permission, presented_object, passed: true)
            end

            expect(resolved).to eq('Resolved value')
          end

          it 'returns nil if the value was not authorized' do
            allow(Ability).to receive(:allowed?).and_return false

            expect(resolved).to be_nil
          end
        end

        context 'when the field is a built-in scalar type' do
          let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) }
          let(:expected_permissions) { [:read_field] }

          it_behaves_like 'checking permissions on the presented object'
        end

        context 'when the field is a list of scalar types' do
          let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) }
          let(:expected_permissions) { [:read_field] }

          it_behaves_like 'checking permissions on the presented object'
        end

        context 'when the field is sub-classed scalar type' do
          let(:type_class) { type_with_field(Types::TimeType, :read_field) }
          let(:expected_permissions) { [:read_field] }

          it_behaves_like 'checking permissions on the presented object'
        end

        context 'when the field is a list of sub-classed scalar types' do
          let(:type_class) { type_with_field([Types::TimeType], :read_field) }
          let(:expected_permissions) { [:read_field] }

          it_behaves_like 'checking permissions on the presented object'
        end
      end

      context 'when the field is a connection' do
        context 'when it resolves to nil' do
          let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) }

          it 'does not fail when authorizing' do
            expect(resolved).to be_nil
          end
        end

        context 'when it returns values' do
          let(:objects) { [1, 2, 3] }
          let(:field_type) { type([:read_object]).connection_type }
          let(:type_class) { type_with_field(field_type, [], objects) }

          it 'filters out unauthorized values' do
            spy_ability_check_for(:read_object, 1, passed: true)
            spy_ability_check_for(:read_object, 2, passed: false)
            spy_ability_check_for(:read_object, 3, passed: true)

            expect(resolved.nodes).to eq [1, 3]
          end
        end
      end

      context 'when the field is a specific type' do
        let(:custom_type) { type(:read_type) }
        let(:object_in_field) { double('presented in field') }

        let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) }
        let(:type_instance) { type_class.authorized_new(object_in_field, context) }

        it 'checks both field & type permissions' do
          spy_ability_check_for(:read_field, object_in_field, passed: true)
          spy_ability_check_for(:read_type, object_in_field, passed: true)

          expect(resolved).to eq(object_in_field)
        end

        it 'returns nil if viewing was not allowed' do
          spy_ability_check_for(:read_field, object_in_field, passed: false)
          spy_ability_check_for(:read_type, object_in_field, passed: true)

          expect(resolved).to be_nil
        end

        context 'when the field is not nullable' do
          let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) }

          it 'returns nil when viewing is not allowed' do
            spy_ability_check_for(:read_type, object_in_field, passed: false)

            expect(resolved).to be_nil
          end
        end

        context 'when the field is a list' do
          let(:object_1) { double('presented in field 1') }
          let(:object_2) { double('presented in field 2') }
          let(:presented_types) { [double(object: object_1), double(object: object_2)] }

          let(:type_class) { type_with_field([custom_type], :read_field, presented_types) }
          let(:type_instance) { type_class.authorized_new(presented_types, context) }

          it 'checks all permissions' do
            allow(Ability).to receive(:allowed?) { true }

            spy_ability_check_for(:read_field, object_1, passed: true)
            spy_ability_check_for(:read_type, object_1, passed: true)
            spy_ability_check_for(:read_field, object_2, passed: true)
            spy_ability_check_for(:read_type, object_2, passed: true)

            expect(resolved).to eq(presented_types)
          end

          it 'filters out objects that the user cannot see' do
            allow(Ability).to receive(:allowed?) { true }

            spy_ability_check_for(:read_type, object_1, passed: false)

            expect(resolved).to contain_exactly(have_attributes(object: object_2))
          end
        end
      end
    end

    it_behaves_like 'authorizing fields'
  end

  private

  def spy_ability_check_for(ability, object, passed: true)
    expect(Ability)
      .to receive(:allowed?)
      .with(current_user, ability, object)
      .and_return(passed)
  end
end