Read about our upcoming Code of Conduct on this issue

This instance was upgraded to Heptapod 0.28.0 today

git_access_spec.rb 33.3 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
2
require 'spec_helper'

3
describe Gitlab::GitAccess do
4
5
  set(:user) { create(:user) }

6
  let(:actor) { user }
7
  let(:project) { create(:project, :repository) }
8
9
  let(:project_path) { project.path }
  let(:namespace_path) { project&.namespace&.path }
10
  let(:protocol) { 'ssh' }
11
  let(:authentication_abilities) { %i[read_project download_code push_code] }
12
  let(:redirected_path) { nil }
13
  let(:auth_result_type) { nil }
14

15
16
17
18
  let(:access) do
    described_class.new(actor, project,
      protocol, authentication_abilities: authentication_abilities,
                namespace_path: namespace_path, project_path: project_path,
19
                redirected_path: redirected_path, auth_result_type: auth_result_type)
20
21
  end

22
23
24
  let(:changes) { '_any' }
  let(:push_access_check) { access.check('git-receive-pack', changes) }
  let(:pull_access_check) { access.check('git-upload-pack', changes) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
25

26
27
  describe '#check with single protocols allowed' do
    def disable_protocol(protocol)
28
      allow(Gitlab::ProtocolAccess).to receive(:allowed?).with(protocol).and_return(false)
29
30
31
32
33
34
35
    end

    context 'ssh disabled' do
      before do
        disable_protocol('ssh')
      end

36
37
38
39
40
      it 'blocks ssh git push and pull' do
        aggregate_failures do
          expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
          expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
        end
41
42
43
44
      end
    end

    context 'http disabled' do
Michael Kozono's avatar
Michael Kozono committed
45
46
      let(:protocol) { 'http' }

47
48
      before do
        disable_protocol('http')
49
        project.add_master(user)
50
51
      end

52
53
54
55
56
      it 'blocks http push and pull' do
        aggregate_failures do
          expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
          expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
        end
57
      end
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

      context 'when request is made from CI' do
        let(:auth_result_type) { :build }

        it "doesn't block http pull" do
          aggregate_failures do
            expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed')
          end
        end

        context 'when legacy CI credentials are used' do
          let(:auth_result_type) { :ci }

          it "doesn't block http pull" do
            aggregate_failures do
              expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed')
            end
          end
        end
      end
78
79
80
    end
  end

81
82
83
84
  describe '#check_project_accessibility!' do
    context 'when the project exists' do
      context 'when actor exists' do
        context 'when actor is a DeployKey' do
85
          let(:deploy_key) { create(:deploy_key, user: user) }
86
87
88
          let(:actor) { deploy_key }

          context 'when the DeployKey has access to the project' do
89
            before do
90
              deploy_key.deploy_keys_projects.create(project: project, can_push: true)
91
            end
92

93
94
95
96
97
            it 'allows push and pull access' do
              aggregate_failures do
                expect { push_access_check }.not_to raise_error
                expect { pull_access_check }.not_to raise_error
              end
98
99
100
101
            end
          end

          context 'when the Deploykey does not have access to the project' do
102
103
            it 'blocks push and pull with "not found"' do
              aggregate_failures do
104
105
                expect { push_access_check }.to raise_not_found
                expect { pull_access_check }.to raise_not_found
106
              end
107
108
109
110
111
112
            end
          end
        end

        context 'when actor is a User' do
          context 'when the User can read the project' do
113
            before do
114
              project.add_master(user)
115
            end
116

117
118
119
120
121
            it 'allows push and pull access' do
              aggregate_failures do
                expect { pull_access_check }.not_to raise_error
                expect { push_access_check }.not_to raise_error
              end
122
123
124
125
            end
          end

          context 'when the User cannot read the project' do
126
127
            it 'blocks push and pull with "not found"' do
              aggregate_failures do
128
129
                expect { push_access_check }.to raise_not_found
                expect { pull_access_check }.to raise_not_found
130
              end
131
132
133
134
135
136
137
138
139
140
            end
          end
        end

        # For backwards compatibility
        context 'when actor is :ci' do
          let(:actor) { :ci }
          let(:authentication_abilities) { build_authentication_abilities }

          it 'allows pull access' do
141
            expect { pull_access_check }.not_to raise_error
142
143
144
          end

          it 'does not block pushes with "not found"' do
145
            expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
146
147
          end
        end
148
149

        context 'when actor is DeployToken' do
Mayra Cabrera's avatar
Mayra Cabrera committed
150
151
          let(:project_deploy_token) { create(:project_deploy_token, project: project) }
          let(:actor) { project_deploy_token.deploy_token }
152

Mayra Cabrera's avatar
Mayra Cabrera committed
153
          context 'when DeployToken is active and belongs to project' do
154
155
156
157
158
159
            it 'allows pull access' do
              expect { pull_access_check }.not_to raise_error
            end
          end

          context 'when DeployToken does not belong to project' do
Mayra Cabrera's avatar
Mayra Cabrera committed
160
            let(:actor) { create(:deploy_token) }
161
162
163
164
165
166

            it 'blocks pull access' do
              expect { pull_access_check }.to raise_not_found
            end
          end
        end
167
168
169
170
171
172
173
174
175
      end

      context 'when actor is nil' do
        let(:actor) { nil }

        context 'when guests can read the project' do
          let(:project) { create(:project, :repository, :public) }

          it 'allows pull access' do
176
            expect { pull_access_check }.not_to raise_error
177
178
179
          end

          it 'does not block pushes with "not found"' do
180
            expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
181
182
183
184
185
          end
        end

        context 'when guests cannot read the project' do
          it 'blocks pulls with "not found"' do
186
            expect { pull_access_check }.to raise_not_found
187
188
189
          end

          it 'blocks pushes with "not found"' do
190
            expect { push_access_check }.to raise_not_found
191
192
193
194
195
196
197
          end
        end
      end
    end

    context 'when the project is nil' do
      let(:project) { nil }
198
      let(:project_path) { "new-project" }
199

200
201
      it 'blocks push and pull with "not found"' do
        aggregate_failures do
202
203
          expect { pull_access_check }.to raise_not_found
          expect { push_access_check }.to raise_not_found
204
        end
205
      end
Tiago Botelho's avatar
Tiago Botelho committed
206
207

      context 'when user is allowed to create project in namespace' do
208
209
210
211
212
213
214
        let(:namespace_path) { user.namespace.path }
        let(:access) do
          described_class.new(actor, nil,
            protocol, authentication_abilities: authentication_abilities,
                      project_path: project_path, namespace_path: namespace_path,
                      redirected_path: redirected_path)
        end
Tiago Botelho's avatar
Tiago Botelho committed
215
216
217
218
219
220
221
222
223
224
225
226

        it 'blocks pull access with "not found"' do
          expect { pull_access_check }.to raise_not_found
        end

        it 'allows push access' do
          expect { push_access_check }.not_to raise_error
        end
      end

      context 'when user is not allowed to create project in namespace' do
        let(:user2) { create(:user) }
227
228
229
230
231
232
233
        let(:namespace_path) { user2.namespace.path }
        let(:access) do
          described_class.new(actor, nil,
            protocol, authentication_abilities: authentication_abilities,
                      project_path: project_path, namespace_path: namespace_path,
                      redirected_path: redirected_path)
        end
Tiago Botelho's avatar
Tiago Botelho committed
234
235
236
237
238
239
240
241

        it 'blocks push and pull with "not found"' do
          aggregate_failures do
            expect { pull_access_check }.to raise_not_found
            expect { push_access_check }.to raise_not_found
          end
        end
      end
242
243
244
    end
  end

245
246
247
248
249
250
251
  shared_examples '#check with a key that is not valid' do
    before do
      project.add_master(user)
    end

    context 'key is too small' do
      before do
252
        stub_application_setting(rsa_key_restriction: 4096)
253
254
      end

255
      it 'does not allow keys which are too small', :aggregate_failures do
Nick Thomas's avatar
Nick Thomas committed
256
257
258
        expect(actor).not_to be_valid
        expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
        expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
259
260
261
262
263
      end
    end

    context 'key type is not allowed' do
      before do
264
        stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
265
266
      end

267
      it 'does not allow keys which are too small', :aggregate_failures do
Nick Thomas's avatar
Nick Thomas committed
268
269
270
        expect(actor).not_to be_valid
        expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
        expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
271
272
273
274
275
276
277
278
279
280
281
282
      end
    end
  end

  it_behaves_like '#check with a key that is not valid' do
    let(:actor) { build(:rsa_key_2048, user: user) }
  end

  it_behaves_like '#check with a key that is not valid' do
    let(:actor) { build(:rsa_deploy_key_2048, user: user) }
  end

283
  shared_examples 'check_project_moved' do
Bob Van Landuyt's avatar
Bob Van Landuyt committed
284
    it 'enqueues a redirected message for pushing' do
285
286
      push_access_check

287
      expect(Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)).not_to be_nil
288
    end
Bob Van Landuyt's avatar
Bob Van Landuyt committed
289
290
291
292
293
294
295

    it 'allows push and pull access' do
      aggregate_failures do
        expect { push_access_check }.not_to raise_error
        expect { pull_access_check }.not_to raise_error
      end
    end
296
297
  end

Bob Van Landuyt's avatar
Bob Van Landuyt committed
298
  describe '#add_project_moved_message!', :clean_gitlab_redis_shared_state do
299
    before do
300
      project.add_master(user)
301
302
303
    end

    context 'when a redirect was not followed to find the project' do
304
305
306
307
308
      it 'allows push and pull access' do
        aggregate_failures do
          expect { push_access_check }.not_to raise_error
          expect { pull_access_check }.not_to raise_error
        end
309
310
311
      end
    end

Bob Van Landuyt's avatar
Bob Van Landuyt committed
312
    context 'with a redirect and ssh protocol' do
313
314
315
316
317
      let(:redirected_path) { 'some/other-path' }

      it_behaves_like 'check_project_moved'
    end

Bob Van Landuyt's avatar
Bob Van Landuyt committed
318
    context 'with a redirect and http protocol' do
319
320
321
322
323
      let(:redirected_path) { 'some/other-path' }
      let(:protocol) { 'http' }

      it_behaves_like 'check_project_moved'
    end
324
325
  end

326
327
328
329
330
331
332
333
334
  describe '#check_authentication_abilities!' do
    before do
      project.add_master(user)
    end

    context 'when download' do
      let(:authentication_abilities) { [] }

      it 'raises unauthorized with download error' do
335
        expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_download])
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
      end

      context 'when authentication abilities include download code' do
        let(:authentication_abilities) { [:download_code] }

        it 'does not raise any errors' do
          expect { pull_access_check }.not_to raise_error
        end
      end

      context 'when authentication abilities include build download code' do
        let(:authentication_abilities) { [:build_download_code] }

        it 'does not raise any errors' do
          expect { pull_access_check }.not_to raise_error
        end
      end
    end

    context 'when upload' do
      let(:authentication_abilities) { [] }

      it 'raises unauthorized with push error' do
359
        expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
360
361
362
363
364
365
366
367
368
369
370
371
      end

      context 'when authentication abilities include push code' do
        let(:authentication_abilities) { [:push_code] }

        it 'does not raise any errors' do
          expect { push_access_check }.not_to raise_error
        end
      end
    end
  end

372
  describe '#check_command_disabled!' do
373
    before do
374
      project.add_master(user)
375
    end
376
377
378
379
380
381
382
383
384
385

    context 'over http' do
      let(:protocol) { 'http' }

      context 'when the git-upload-pack command is disabled in config' do
        before do
          allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
        end

        context 'when calling git-upload-pack' do
Michael Kozono's avatar
Michael Kozono committed
386
          it { expect { pull_access_check }.to raise_unauthorized('Pulling over HTTP is not allowed.') }
387
388
389
        end

        context 'when calling git-receive-pack' do
390
          it { expect { push_access_check }.not_to raise_error }
391
392
393
394
395
396
397
398
399
        end
      end

      context 'when the git-receive-pack command is disabled in config' do
        before do
          allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
        end

        context 'when calling git-receive-pack' do
Michael Kozono's avatar
Michael Kozono committed
400
          it { expect { push_access_check }.to raise_unauthorized('Pushing over HTTP is not allowed.') }
401
402
403
        end

        context 'when calling git-upload-pack' do
404
          it { expect { pull_access_check }.not_to raise_error }
405
406
407
408
409
        end
      end
    end
  end

410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
  describe '#check_db_accessibility!' do
    context 'when in a read-only GitLab instance' do
      before do
        create(:protected_branch, name: 'feature', project: project)
        allow(Gitlab::Database).to receive(:read_only?) { true }
      end

      it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) }
    end
  end

  describe '#ensure_project_on_push!' do
    let(:access) do
      described_class.new(actor, project,
        protocol, authentication_abilities: authentication_abilities,
                  project_path: project_path, namespace_path: namespace_path,
                  redirected_path: redirected_path)
    end

    context 'when push' do
      let(:cmd) { 'git-receive-pack' }

      context 'when project does not exist' do
        let(:project_path) { "nonexistent" }
        let(:project) { nil }

        context 'when changes is _any' do
          let(:changes) { '_any' }

          context 'when authentication abilities include push code' do
            let(:authentication_abilities) { [:push_code] }

            context 'when user can create project in namespace' do
              let(:namespace_path) { user.namespace.path }

              it 'creates a new project' do
                expect { access.send(:ensure_project_on_push!, cmd, changes) }.to change { Project.count }.by(1)
              end
            end

            context 'when user cannot create project in namespace' do
              let(:user2) { create(:user) }
              let(:namespace_path) { user2.namespace.path }

              it 'does not create a new project' do
                expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
              end
            end
          end

          context 'when authentication abilities do not include push code' do
            let(:authentication_abilities) { [] }

            context 'when user can create project in namespace' do
              let(:namespace_path) { user.namespace.path }

              it 'does not create a new project' do
                expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
              end
            end
          end
Tiago Botelho's avatar
Tiago Botelho committed
471
472
        end

473
474
475
476
477
        context 'when check contains actual changes' do
          let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }

          it 'does not create a new project' do
            expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
Tiago Botelho's avatar
Tiago Botelho committed
478
479
480
          end
        end
      end
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502

      context 'when project exists' do
        let(:changes) { '_any' }
        let!(:project) { create(:project) }

        it 'does not create a new project' do
          expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
        end
      end

      context 'when deploy key is used' do
        let(:key) { create(:deploy_key, user: user) }
        let(:actor) { key }
        let(:project_path) { "nonexistent" }
        let(:project) { nil }
        let(:namespace_path) { user.namespace.path }
        let(:changes) { '_any' }

        it 'does not create a new project' do
          expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
        end
      end
Tiago Botelho's avatar
Tiago Botelho committed
503
504
    end

505
506
507
    context 'when pull' do
      let(:cmd) { 'git-upload-pack' }
      let(:changes) { '_any' }
Tiago Botelho's avatar
Tiago Botelho committed
508

509
510
511
512
513
514
515
      context 'when project does not exist' do
        let(:project_path) { "new-project" }
        let(:namespace_path) { user.namespace.path }
        let(:project) { nil }

        it 'does not create a new project' do
          expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
Tiago Botelho's avatar
Tiago Botelho committed
516
517
518
519
520
        end
      end
    end
  end

521
  describe '#check_download_access!' do
522
523
    it 'allows masters to pull' do
      project.add_master(user)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
524

525
      expect { pull_access_check }.not_to raise_error
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
526
527
    end

528
529
    it 'disallows guests to pull' do
      project.add_guest(user)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
530

531
      expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
532
533
    end

534
535
536
    it 'disallows blocked users to pull' do
      project.add_master(user)
      user.block
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
537

538
      expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
539
540
    end

541
542
543
544
545
546
547
548
549
550
551
552
553
    context 'when the project repository does not exist' do
      it 'returns not found' do
        project.add_guest(user)
        repo = project.repository
        FileUtils.rm_rf(repo.path)

        # Sanity check for rm_rf
        expect(repo.exists?).to eq(false)

        expect { pull_access_check }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.')
      end
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
554
    describe 'without access to project' do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
555
      context 'pull code' do
556
        it { expect { pull_access_check }.to raise_not_found }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
557
      end
558
559

      context 'when project is public' do
560
        let(:public_project) { create(:project, :public, :repository) }
561
562
563
        let(:project_path) { public_project.path }
        let(:namespace_path) { public_project.namespace.path }
        let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], project_path: project_path, namespace_path: namespace_path) }
564
565
566

        context 'when repository is enabled' do
          it 'give access to download code' do
567
            expect { pull_access_check }.not_to raise_error
568
569
570
571
572
573
574
          end
        end

        context 'when repository is disabled' do
          it 'does not give access to download code' do
            public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)

575
            expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
576
577
578
          end
        end
      end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
579
    end
580
581

    describe 'deploy key permissions' do
582
      let(:key) { create(:deploy_key, user: user) }
583
      let(:actor) { key }
584
585

      context 'pull code' do
586
        context 'when project is authorized' do
587
588
589
          before do
            key.projects << project
          end
590

591
          it { expect { pull_access_check }.not_to raise_error }
592
593
594
595
        end

        context 'when unauthorized' do
          context 'from public project' do
596
            let(:project) { create(:project, :public, :repository) }
597

598
            it { expect { pull_access_check }.not_to raise_error }
599
600
601
          end

          context 'from internal project' do
602
            let(:project) { create(:project, :internal, :repository) }
603

604
            it { expect { pull_access_check }.to raise_not_found }
605
606
607
          end

          context 'from private project' do
608
            let(:project) { create(:project, :private, :repository) }
609

610
            it { expect { pull_access_check }.to raise_not_found }
611
612
          end
        end
613
614
      end
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
615

616
617
    describe 'build authentication_abilities permissions' do
      let(:authentication_abilities) { build_authentication_abilities }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
618

619
      describe 'owner' do
620
        let(:project) { create(:project, :repository, namespace: user.namespace) }
621
622

        context 'pull code' do
623
          it { expect { pull_access_check }.not_to raise_error }
624
625
626
        end
      end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
627
      describe 'reporter user' do
628
        before do
629
          project.add_reporter(user)
630
        end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
631
632

        context 'pull code' do
633
          it { expect { pull_access_check }.not_to raise_error }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
634
635
636
637
638
639
640
        end
      end

      describe 'admin user' do
        let(:user) { create(:admin) }

        context 'when member of the project' do
641
          before do
642
            project.add_reporter(user)
643
          end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
644
645

          context 'pull code' do
646
            it { expect { pull_access_check }.not_to raise_error }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
647
648
649
650
651
          end
        end

        context 'when is not member of the project' do
          context 'pull code' do
652
            it { expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
653
654
655
          end
        end
      end
656
657
658
659
660

      describe 'generic CI (build without a user)' do
        let(:actor) { :ci }

        context 'pull code' do
661
          it { expect { pull_access_check }.not_to raise_error }
662
663
        end
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
664
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
665
666
  end

667
668
669
670
671
672
673
674
675
676
677
678
679
680
  describe 'check LFS integrity' do
    let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] }

    before do
      project.add_developer(user)
    end

    it 'checks LFS integrity only for first change' do
      expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times

      push_access_check
    end
  end

681
  describe '#check_push_access!' do
682
683
684
    before do
      merge_into_protected_branch
    end
685
    let(:unprotected_branch) { 'unprotected_branch' }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
686

687
688
    let(:changes) do
      { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
689
690
        push_master: '6f6d7e7ed 570e7b2ab refs/heads/master',
        push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature',
691
692
        push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\
                                      'refs/heads/feature',
693
        push_tag: '6f6d7e7ed 570e7b2ab refs/tags/v1.0.0',
694
        push_new_tag: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/tags/v7.8.9",
695
696
        push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'],
        merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" }
697
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
698

699
700
    def stub_git_hooks
      # Running the `pre-receive` hook is expensive, and not necessary for this test.
701
      allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) do |service, &block|
Sean McGivern's avatar
Sean McGivern committed
702
703
        block.call(service)
      end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
704
    end
705

706
707
708
709
710
    def merge_into_protected_branch
      @protected_branch_merge_commit ||= begin
        stub_git_hooks
        project.repository.add_branch(user, unprotected_branch, 'feature')
        target_branch = project.repository.lookup('feature')
711
        source_branch = project.repository.create_file(
Lin Jen-Shin's avatar
Lin Jen-Shin committed
712
          user,
Douwe Maan's avatar
Douwe Maan committed
713
          'filename',
714
715
          'This is the file content',
          message: 'This is a good commit message',
716
          branch_name: unprotected_branch)
717
718
        rugged = project.repository.rugged
        author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
719

720
721
        merge_index = rugged.merge_commits(target_branch, source_branch)
        Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
722
723
      end
    end
724

725
    def self.run_permission_checks(permissions_matrix)
726
727
728
729
730
731
732
733
734
735
      permissions_matrix.each_pair do |role, matrix|
        # Run through the entire matrix for this role in one test to avoid
        # repeated setup.
        #
        # Expectations are given a custom failure message proc so that it's
        # easier to identify which check(s) failed.
        it "has the correct permissions for #{role}s" do
          if role == :admin
            user.update_attribute(:admin, true)
          else
736
            project.add_role(user, role)
Sean McGivern's avatar
Sean McGivern committed
737
738
          end

739
          aggregate_failures do
740
741
742
743
744
745
746
747
748
            matrix.each do |action, allowed|
              check = -> { access.send(:check_push_access!, changes[action]) }

              if allowed
                expect(&check).not_to raise_error,
                  -> { "expected #{action} to be allowed" }
              else
                expect(&check).to raise_error(Gitlab::GitAccess::UnauthorizedError),
                  -> { "expected #{action} to be disallowed" }
749
              end
750
751
752
753
754
            end
          end
        end
      end
    end
755
756

    permissions_matrix = {
Timothy Andrew's avatar
Timothy Andrew committed
757
758
759
760
761
762
763
764
765
766
767
      admin: {
        push_new_branch: true,
        push_master: true,
        push_protected_branch: true,
        push_remove_protected_branch: false,
        push_tag: true,
        push_new_tag: true,
        push_all: true,
        merge_into_protected_branch: true
      },

768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
      master: {
        push_new_branch: true,
        push_master: true,
        push_protected_branch: true,
        push_remove_protected_branch: false,
        push_tag: true,
        push_new_tag: true,
        push_all: true,
        merge_into_protected_branch: true
      },

      developer: {
        push_new_branch: true,
        push_master: true,
        push_protected_branch: false,
        push_remove_protected_branch: false,
        push_tag: false,
        push_new_tag: true,
        push_all: false,
        merge_into_protected_branch: false
      },

      reporter: {
        push_new_branch: false,
        push_master: false,
        push_protected_branch: false,
        push_remove_protected_branch: false,
        push_tag: false,
        push_new_tag: false,
        push_all: false,
        merge_into_protected_branch: false
      },

      guest: {
        push_new_branch: false,
        push_master: false,
        push_protected_branch: false,
        push_remove_protected_branch: false,
        push_tag: false,
        push_new_tag: false,
        push_all: false,
        merge_into_protected_branch: false
      }
    }

Douwe Maan's avatar
Douwe Maan committed
813
    [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
814
      context do
815
816
817
        before do
          create(:protected_branch, name: protected_branch_name, project: project)
        end
818
819
820
821

        run_permission_checks(permissions_matrix)
      end

822
      context "when developers are allowed to push into the #{protected_branch_type} protected branch" do
823
824
825
        before do
          create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project)
        end
826
827
828
829

        run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
      end

830
      context "developers are allowed to merge into the #{protected_branch_type} protected branch" do
831
832
833
        before do
          create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project)
        end
834
835
836
837

        context "when a merge request exists for the given source/target branch" do
          context "when the merge request is in progress" do
            before do
838
              create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature',
839
                                     state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch)
840
841
            end

842
843
            run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true }))
          end
844

845
846
847
          context "when the merge request is not in progress" do
            before do
              create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil)
848
            end
849
850

            run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
851
          end
852

853
          context "when a merge request does not exist for the given source/target branch" do
854
855
856
857
858
            run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
          end
        end
      end

859
      context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do
860
861
862
        before do
          create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project)
        end
863
864
865

        run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
      end
866

Timothy Andrew's avatar
Timothy Andrew committed
867
      context "when no one is allowed to push to the #{protected_branch_name} protected branch" do
868
869
870
        before do
          create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project)
        end
871

Timothy Andrew's avatar
Timothy Andrew committed
872
873
874
875
        run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
                                                            master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
                                                            admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
      end
876
    end
877
878
879
880
881
882
883
884
885
886
887
888
889
890

    context 'when pushing to a project' do
      let(:project) { create(:project, :public, :repository) }
      let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow" }

      before do
        project.add_developer(user)
      end

      it 'cleans up the files' do
        expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
        expect { push_access_check }.not_to raise_error
      end
    end
891
  end
892

893
894
  describe 'build authentication abilities' do
    let(:authentication_abilities) { build_authentication_abilities }
895

Kamil Trzcinski's avatar
Kamil Trzcinski committed
896
    context 'when project is authorized' do
897
      before do
898
        project.add_reporter(user)
899
      end
900

901
      it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
902
903
904
905
    end

    context 'when unauthorized' do
      context 'to public project' do
906
        let(:project) { create(:project, :public, :repository) }
907

908
        it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
909
      end
910

Kamil Trzcinski's avatar
Kamil Trzcinski committed
911
      context 'to internal project' do
912
        let(:project) { create(:project, :internal, :repository) }
913

914
        it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
915
      end
916

Kamil Trzcinski's avatar
Kamil Trzcinski committed
917
      context 'to private project' do
918
        let(:project) { create(:project, :private, :repository) }
919

920
        it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
921
922
      end
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
923
924
  end

Toon Claes's avatar
Toon Claes committed
925
926
927
928
929
930
931
932
933
934
  context 'when the repository is read only' do
    let(:project) { create(:project, :repository, :read_only) }

    it 'denies push access' do
      project.add_master(user)

      expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
    end
  end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
935
  describe 'deploy key permissions' do
936
    let(:key) { create(:deploy_key, user: user) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
937
938
    let(:actor) { key }

939
    context 'when deploy_key can push' do
940
      context 'when project is authorized' do
941
        before do
942
          key.deploy_keys_projects.create(project: project, can_push: true)
943
        end
944
945
946
947
948
949
950
951

        it { expect { push_access_check }.not_to raise_error }
      end

      context 'when unauthorized' do
        context 'to public project' do
          let(:project) { create(:project, :public, :repository) }

952
          it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
953
954
955
956
957
        end

        context 'to internal project' do
          let(:project) { create(:project, :internal, :repository) }

958
          it { expect { push_access_check }.to raise_not_found }
959
960
961
962
963
        end

        context 'to private project' do
          let(:project) { create(:project, :private, :repository) }

964
          it { expect { push_access_check }.to raise_not_found }
965
966
967
968
969
        end
      end
    end

    context 'when deploy_key cannot push' do
970
      context 'when project is authorized' do
971
        before do
972
          key.deploy_keys_projects.create(project: project, can_push: false)
973
        end
974

975
        it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
976
977
978
979
980
981
      end

      context 'when unauthorized' do
        context 'to public project' do
          let(:project) { create(:project, :public, :repository) }

982
          it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
983
984
985
986
987
        end

        context 'to internal project' do
          let(:project) { create(:project, :internal, :repository) }

988
          it { expect { push_access_check }.to raise_not_found }
989
990
991
992
993
        end

        context 'to private project' do
          let(:project) { create(:project, :private, :repository) }

994
          it { expect { push_access_check }.to raise_not_found }
995
        end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
996
997
      end
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
998
999
1000
1001
  end

  private

1002
1003
1004
1005
  def raise_unauthorized(message)
    raise_error(Gitlab::GitAccess::UnauthorizedError, message)
  end

1006
  def raise_not_found
1007
    raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
1008
1009
  end

1010
  def build_authentication_abilities
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1011
1012
1013
1014
1015
    [
      :read_project,
      :build_download_code
    ]
  end
1016

1017
  def full_authentication_abilities
1018
1019
1020
1021
1022
1023
    [
      :read_project,
      :download_code,
      :push_code
    ]
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1024
end