authorization_spec.rb 9.23 KB
Newer Older
1 2 3 4 5 6 7
# frozen_string_literal: true

require 'spec_helper'

describe 'Gitlab::Graphql::Authorization' do
  set(:user) { create(:user) }

Luke Duncalfe's avatar
Luke Duncalfe committed
8 9
  let(:permission_single) { :foo }
  let(:permission_collection) { [:foo, :bar] }
10
  let(:test_object) { double(name: 'My name') }
11
  let(:query_string) { '{ item() { name } }' }
Luke Duncalfe's avatar
Luke Duncalfe committed
12
  let(:result) { execute_query(query_type)['data'] }
13

14
  subject { result['item'] }
Luke Duncalfe's avatar
Luke Duncalfe committed
15 16 17 18 19 20 21 22 23 24 25

  shared_examples 'authorization with a single permission' do
    it 'returns the protected field when user has permission' do
      permit(permission_single)

      expect(subject).to eq('name' => test_object.name)
    end

    it 'returns nil when user is not authorized' do
      expect(subject).to be_nil
    end
26 27
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
  shared_examples 'authorization with a collection of permissions' do
    it 'returns the protected field when user has all permissions' do
      permit(*permission_collection)

      expect(subject).to eq('name' => test_object.name)
    end

    it 'returns nil when user only has one of the permissions' do
      permit(permission_collection.first)

      expect(subject).to be_nil
    end

    it 'returns nil when user only has none of the permissions' do
      expect(subject).to be_nil
    end
  end
45 46 47 48 49 50

  before do
    # By default, disallow all permissions.
    allow(Ability).to receive(:allowed?).and_return(false)
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
51 52
  describe 'Field authorizations' do
    let(:type) { type_factory }
53

Luke Duncalfe's avatar
Luke Duncalfe committed
54 55 56
    describe 'with a single permission' do
      let(:query_type) do
        query_factory do |query|
57
          query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_single
Luke Duncalfe's avatar
Luke Duncalfe committed
58 59 60 61 62 63 64 65 66 67
        end
      end

      include_examples 'authorization with a single permission'
    end

    describe 'with a collection of permissions' do
      let(:query_type) do
        permissions = permission_collection
        query_factory do |qt|
68
          qt.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
Luke Duncalfe's avatar
Luke Duncalfe committed
69 70 71 72
            authorize permissions
          end
        end
      end
73

Luke Duncalfe's avatar
Luke Duncalfe committed
74 75 76
      include_examples 'authorization with a collection of permissions'
    end
  end
77

78 79 80
  describe 'Field authorizations when field is a built in type' do
    let(:query_type) do
      query_factory do |query|
81
        query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }
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
      end
    end

    describe 'with a single permission' do
      let(:type) do
        type_factory do |type|
          type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
        end
      end

      it 'returns the protected field when user has permission' do
        permit(permission_single)

        expect(subject).to eq('name' => test_object.name)
      end

      it 'returns nil when user is not authorized' do
        expect(subject).to eq('name' => nil)
      end
    end

    describe 'with a collection of permissions' do
      let(:type) do
        permissions = permission_collection
        type_factory do |type|
          type.field :name, GraphQL::STRING_TYPE, null: true do
            authorize permissions
          end
        end
      end

      it 'returns the protected field when user has all permissions' do
        permit(*permission_collection)

        expect(subject).to eq('name' => test_object.name)
      end

      it 'returns nil when user only has one of the permissions' do
        permit(permission_collection.first)

        expect(subject).to eq('name' => nil)
      end

      it 'returns nil when user only has none of the permissions' do
        expect(subject).to eq('name' => nil)
      end
    end
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
131 132 133
  describe 'Type authorizations' do
    let(:query_type) do
      query_factory do |query|
134
        query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }
Luke Duncalfe's avatar
Luke Duncalfe committed
135
      end
136 137
    end

Luke Duncalfe's avatar
Luke Duncalfe committed
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    describe 'with a single permission' do
      let(:type) do
        type_factory do |type|
          type.authorize permission_single
        end
      end

      include_examples 'authorization with a single permission'
    end

    describe 'with a collection of permissions' do
      let(:type) do
        type_factory do |type|
          type.authorize permission_collection
        end
      end

      include_examples 'authorization with a collection of permissions'
156 157 158
    end
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
159 160 161
  describe 'type and field authorizations together' do
    let(:permission_1) { permission_collection.first }
    let(:permission_2) { permission_collection.last }
162

Luke Duncalfe's avatar
Luke Duncalfe committed
163 164 165 166 167
    let(:type) do
      type_factory do |type|
        type.authorize permission_1
      end
    end
168

Luke Duncalfe's avatar
Luke Duncalfe committed
169 170
    let(:query_type) do
      query_factory do |query|
171
        query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
Luke Duncalfe's avatar
Luke Duncalfe committed
172 173
      end
    end
174

Luke Duncalfe's avatar
Luke Duncalfe committed
175 176 177 178
    include_examples 'authorization with a collection of permissions'
  end

  describe 'type authorizations when applied to a relay connection' do
179
    let(:query_string) { '{ item() { edges { node { name } } } }' }
180
    let(:second_test_object) { double(name: 'Second thing') }
Luke Duncalfe's avatar
Luke Duncalfe committed
181 182 183 184 185 186 187 188 189

    let(:type) do
      type_factory do |type|
        type.authorize permission_single
      end
    end

    let(:query_type) do
      query_factory do |query|
190
        query.field :item, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object, second_test_object] }
Luke Duncalfe's avatar
Luke Duncalfe committed
191
      end
192 193
    end

194
    subject { result.dig('item', 'edges') }
195

196
    it 'returns only the elements visible to the user' do
Luke Duncalfe's avatar
Luke Duncalfe committed
197 198
      permit(permission_single)

199
      expect(subject.size).to eq 1
Luke Duncalfe's avatar
Luke Duncalfe committed
200
      expect(subject.first['node']).to eq('name' => test_object.name)
201 202
    end

Luke Duncalfe's avatar
Luke Duncalfe committed
203 204 205
    it 'returns nil when user is not authorized' do
      expect(subject).to be_empty
    end
206 207 208 209

    describe 'limiting connections with multiple objects' do
      let(:query_type) do
        query_factory do |query|
210
          query.field :item, type.connection_type, null: true, resolve: ->(obj, args, ctx) do
211 212 213 214 215
            [test_object, second_test_object]
          end
        end
      end

216
      let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' }
217 218 219 220 221 222 223 224

      it 'only checks permissions for the first object' do
        expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true }
        expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object)

        expect(subject.size).to eq(1)
      end
    end
Luke Duncalfe's avatar
Luke Duncalfe committed
225 226 227 228 229 230 231 232 233 234 235
  end

  describe 'type authorizations when applied to a basic connection' do
    let(:type) do
      type_factory do |type|
        type.authorize permission_single
      end
    end

    let(:query_type) do
      query_factory do |query|
236
        query.field :item, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
Luke Duncalfe's avatar
Luke Duncalfe committed
237 238 239
      end
    end

240
    subject { result['item'].first }
Luke Duncalfe's avatar
Luke Duncalfe committed
241 242 243 244

    include_examples 'authorization with a single permission'
  end

245 246 247 248 249 250
  describe 'Authorizations on active record relations' do
    let!(:visible_project) { create(:project, :private) }
    let!(:other_project) { create(:project, :private) }
    let!(:visible_issues) { create_list(:issue, 2, project: visible_project) }
    let!(:other_issues) { create_list(:issue, 2, project: other_project) }
    let!(:user) { visible_project.owner }
Luke Duncalfe's avatar
Luke Duncalfe committed
251

252 253 254 255 256
    let(:issue_type) do
      type_factory do |type|
        type.graphql_name 'FakeIssueType'
        type.authorize :read_issue
        type.field :id, GraphQL::ID_TYPE, null: false
Luke Duncalfe's avatar
Luke Duncalfe committed
257
      end
258 259
    end
    let(:project_type) do |type|
Luke Duncalfe's avatar
Luke Duncalfe committed
260
      type_factory do |type|
261 262
        type.graphql_name 'FakeProjectType'
        type.field :test_issues, issue_type.connection_type, null: false, resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]) }
Luke Duncalfe's avatar
Luke Duncalfe committed
263 264 265 266
      end
    end
    let(:query_type) do
      query_factory do |query|
267
        query.field :test_project, project_type, null: false, resolve: -> (_, _, _) { visible_project }
Luke Duncalfe's avatar
Luke Duncalfe committed
268 269
      end
    end
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
    let(:query_string) do
      <<~QRY
        { testProject { testIssues(first: 3) { edges { node { id } } } } }
      QRY
    end

    before do
      allow(Ability).to receive(:allowed?).and_call_original
    end

    it 'renders the issues the user has access to' do
      issue_edges = result['testProject']['testIssues']['edges']
      issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }

      expect(issue_edges.size).to eq(visible_issues.size)
285
      expect(issue_ids).to eq(visible_issues.map { |i| i.to_global_id.to_s })
286 287 288 289
    end

    it 'does not check access on fields that will not be rendered' do
      expect(Ability).not_to receive(:allowed?).with(user, :read_issue, other_issues.last)
Luke Duncalfe's avatar
Luke Duncalfe committed
290

291
      result
292 293 294 295 296 297 298 299 300 301 302
    end
  end

  private

  def permit(*permissions)
    permissions.each do |permission|
      allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
    end
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
303
  def type_factory
304
    Class.new(Types::BaseObject) do
Luke Duncalfe's avatar
Luke Duncalfe committed
305
      graphql_name 'TestType'
306 307

      field :name, GraphQL::STRING_TYPE, null: true
Luke Duncalfe's avatar
Luke Duncalfe committed
308 309

      yield(self) if block_given?
310 311 312
    end
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
313
  def query_factory
314 315 316
    Class.new(Types::BaseObject) do
      graphql_name 'TestQuery'

Luke Duncalfe's avatar
Luke Duncalfe committed
317
      yield(self) if block_given?
318 319 320
    end
  end

Luke Duncalfe's avatar
Luke Duncalfe committed
321 322
  def execute_query(query_type)
    schema = Class.new(GraphQL::Schema) do
323
      use Gitlab::Graphql::Authorize
324 325
      use Gitlab::Graphql::Connections

Luke Duncalfe's avatar
Luke Duncalfe committed
326
      query(query_type)
327
    end
Luke Duncalfe's avatar
Luke Duncalfe committed
328 329 330 331 332 333

    schema.execute(
      query_string,
      context: { current_user: user },
      variables: {}
    )
334 335
  end
end