BigW Consortium Gitlab

git_http_spec.rb 20.8 KB
Newer Older
Douwe Maan committed
1 2
require "spec_helper"

3
describe 'Git HTTP requests', lib: true do
4
  include GitHttpHelpers
5 6
  include WorkhorseHelpers

Jacob Vosmaer committed
7 8 9 10 11 12
  it "gives WWW-Authenticate hints" do
    clone_get('doesnt/exist.git')

    expect(response.header['WWW-Authenticate']).to start_with('Basic ')
  end

13 14
  describe "User with no identities" do
    let(:user)    { create(:user) }
15
    let(:project) { create(:project, :repository, path: 'project.git-project') }
Douwe Maan committed
16

17 18 19 20
    context "when the project doesn't exist" do
      context "when no authentication is provided" do
        it "responds with status 401 (no project existence information leak)" do
          download('doesnt/exist.git') do |response|
21
            expect(response).to have_http_status(401)
Douwe Maan committed
22 23
          end
        end
24
      end
Douwe Maan committed
25

26 27 28 29 30 31
      context "when username and password are provided" do
        context "when authentication fails" do
          it "responds with status 401" do
            download('doesnt/exist.git', user: user.username, password: "nope") do |response|
              expect(response).to have_http_status(401)
            end
Douwe Maan committed
32 33
          end
        end
34

35 36 37 38 39 40 41
        context "when authentication succeeds" do
          it "responds with status 404" do
            download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
              expect(response).to have_http_status(404)
            end
          end
        end
42 43 44
      end
    end

45 46 47
    context "when the Wiki for a project exists" do
      it "responds with the right project" do
        wiki = ProjectWiki.new(project)
48
        project.update_attribute(:visibility_level, Project::PUBLIC)
Jacob Vosmaer committed
49

50 51 52
        download("/#{wiki.repository.path_with_namespace}.git") do |response|
          json_body = ActiveSupport::JSON.decode(response.body)

53
          expect(response).to have_http_status(200)
54
          expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
55
          expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
56
        end
57
      end
58 59

      context 'but the repo is disabled' do
60
        let(:project) { create(:project, :repository_disabled, :wiki_enabled) }
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
        let(:wiki) { ProjectWiki.new(project) }
        let(:path) { "/#{wiki.repository.path_with_namespace}.git" }

        before do
          project.team << [user, :developer]
        end

        it 'allows clones' do
          download(path, user: user.username, password: user.password) do |response|
            expect(response).to have_http_status(200)
          end
        end

        it 'allows pushes' do
          upload(path, user: user.username, password: user.password) do |response|
            expect(response).to have_http_status(200)
          end
        end
      end
80 81 82 83
    end

    context "when the project exists" do
      let(:path) { "#{project.path_with_namespace}.git" }
Jacob Vosmaer committed
84

85 86 87
      context "when the project is public" do
        before do
          project.update_attribute(:visibility_level, Project::PUBLIC)
Jacob Vosmaer committed
88
        end
89

90 91 92 93 94 95
        it "downloads get status 200" do
          download(path, {}) do |response|
            expect(response).to have_http_status(200)
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
          end
        end
Jacob Vosmaer committed
96

97 98 99
        it "uploads get status 401" do
          upload(path, {}) do |response|
            expect(response).to have_http_status(401)
Jacob Vosmaer committed
100 101
          end
        end
102

103 104
        context "with correct credentials" do
          let(:env) { { user: user.username, password: user.password } }
105

106
          it "uploads get status 403" do
Jacob Vosmaer committed
107
            upload(path, env) do |response|
108
              expect(response).to have_http_status(403)
Jacob Vosmaer committed
109 110 111
            end
          end

112 113 114
          context 'but git-receive-pack is disabled' do
            it "responds with status 404" do
              allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
115

116 117 118 119
              upload(path, env) do |response|
                expect(response).to have_http_status(403)
              end
            end
120
          end
Douwe Maan committed
121
        end
122

123 124 125
        context 'but git-upload-pack is disabled' do
          it "responds with status 404" do
            allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
126

127 128 129
            download(path, {}) do |response|
              expect(response).to have_http_status(404)
            end
130 131
          end
        end
132

133 134 135 136 137
        context 'when the request is not from gitlab-workhorse' do
          it 'raises an exception' do
            expect do
              get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
            end.to raise_error(JWT::DecodeError)
138 139
          end
        end
140 141 142 143

        context 'when the repo is public' do
          context 'but the repo is disabled' do
            it 'does not allow to clone the repo' do
144
              project = create(:project, :public, :repository_disabled)
145 146 147 148 149 150 151 152 153

              download("#{project.path_with_namespace}.git", {}) do |response|
                expect(response).to have_http_status(:unauthorized)
              end
            end
          end

          context 'but the repo is enabled' do
            it 'allows to clone the repo' do
154
              project = create(:project, :public, :repository_enabled)
155 156 157 158 159 160 161 162 163

              download("#{project.path_with_namespace}.git", {}) do |response|
                expect(response).to have_http_status(:ok)
              end
            end
          end

          context 'but only project members are allowed' do
            it 'does not allow to clone the repo' do
164
              project = create(:project, :public, :repository_private)
165 166 167 168 169 170 171

              download("#{project.path_with_namespace}.git", {}) do |response|
                expect(response).to have_http_status(:unauthorized)
              end
            end
          end
        end
Douwe Maan committed
172 173
      end

174 175 176 177
      context "when the project is private" do
        before do
          project.update_attribute(:visibility_level, Project::PRIVATE)
        end
Douwe Maan committed
178

179 180 181
        context "when no authentication is provided" do
          it "responds with status 401 to downloads" do
            download(path, {}) do |response|
182
              expect(response).to have_http_status(401)
183
            end
Douwe Maan committed
184 185
          end

186 187
          it "responds with status 401 to uploads" do
            upload(path, {}) do |response|
188
              expect(response).to have_http_status(401)
189
            end
Douwe Maan committed
190
          end
191
        end
Douwe Maan committed
192

193 194
        context "when username and password are provided" do
          let(:env) { { user: user.username, password: 'nope' } }
195

196 197 198 199 200
          context "when authentication fails" do
            it "responds with status 401" do
              download(path, env) do |response|
                expect(response).to have_http_status(401)
              end
201
            end
Douwe Maan committed
202

203 204 205 206
            context "when the user is IP banned" do
              it "responds with status 401" do
                expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
                allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
Douwe Maan committed
207

208 209 210
                clone_get(path, env)

                expect(response).to have_http_status(401)
Douwe Maan committed
211
              end
212
            end
213
          end
Douwe Maan committed
214

215 216
          context "when authentication succeeds" do
            let(:env) { { user: user.username, password: user.password } }
217

218 219 220 221
            context "when the user has access to the project" do
              before do
                project.team << [user, :master]
              end
222

223 224 225 226 227 228 229 230 231
              context "when the user is blocked" do
                it "responds with status 404" do
                  user.block
                  project.team << [user, :master]

                  download(path, env) do |response|
                    expect(response).to have_http_status(404)
                  end
                end
Douwe Maan committed
232
              end
233

234 235 236 237 238 239
              context "when the user isn't blocked" do
                it "downloads get status 200" do
                  expect(Rack::Attack::Allow2Ban).to receive(:reset)

                  clone_get(path, env)

240
                  expect(response).to have_http_status(200)
241
                  expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
242
                end
243

244 245 246 247 248 249
                it "uploads get status 200" do
                  upload(path, env) do |response|
                    expect(response).to have_http_status(200)
                    expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
                  end
                end
250 251
              end

252 253 254
              context "when an oauth token is provided" do
                before do
                  application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
255
                  @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
256
                end
257

258 259
                it "downloads get status 200" do
                  clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
260

261 262 263
                  expect(response).to have_http_status(200)
                  expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
                end
264

265 266 267 268 269
                it "uploads get status 401 (no project existence information leak)" do
                  push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token

                  expect(response).to have_http_status(401)
                end
270 271
              end

272 273 274
              context 'when user has 2FA enabled' do
                let(:user) { create(:user, :two_factor) }
                let(:access_token) { create(:personal_access_token, user: user) }
275

276 277 278
                before do
                  project.team << [user, :master]
                end
279

280 281 282 283 284 285
                context 'when username and password are provided' do
                  it 'rejects the clone attempt' do
                    download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
                      expect(response).to have_http_status(401)
                      expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
                    end
286 287
                  end

288 289 290 291 292
                  it 'rejects the push attempt' do
                    upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
                      expect(response).to have_http_status(401)
                      expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
                    end
293 294 295
                  end
                end

296 297 298 299 300
                context 'when username and personal access token are provided' do
                  it 'allows clones' do
                    download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
                      expect(response).to have_http_status(200)
                    end
301 302
                  end

303 304 305 306
                  it 'allows pushes' do
                    upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
                      expect(response).to have_http_status(200)
                    end
307 308 309 310
                  end
                end
              end

311 312 313 314 315 316
              context "when blank password attempts follow a valid login" do
                def attempt_login(include_password)
                  password = include_password ? user.password : ""
                  clone_get path, user: user.username, password: password
                  response.status
                end
317

318 319 320 321
                it "repeated attempts followed by successful attempt" do
                  options = Gitlab.config.rack_attack.git_basic_auth
                  maxretry = options[:maxretry] - 1
                  ip = '1.2.3.4'
322

323 324
                  allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
                  Rack::Attack::Allow2Ban.reset(ip, options)
325

326 327 328
                  maxretry.times.each do
                    expect(attempt_login(false)).to eq(401)
                  end
329

330 331
                  expect(attempt_login(true)).to eq(200)
                  expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
332

333 334 335
                  maxretry.times.each do
                    expect(attempt_login(false)).to eq(401)
                  end
336

337 338
                  Rack::Attack::Allow2Ban.reset(ip, options)
                end
339
              end
Douwe Maan committed
340 341
            end

342 343 344 345 346
            context "when the user doesn't have access to the project" do
              it "downloads get status 404" do
                download(path, user: user.username, password: user.password) do |response|
                  expect(response).to have_http_status(404)
                end
Douwe Maan committed
347
              end
348

349 350 351 352
              it "uploads get status 404" do
                upload(path, user: user.username, password: user.password) do |response|
                  expect(response).to have_http_status(404)
                end
Jacob Vosmaer committed
353 354
              end
            end
Douwe Maan committed
355 356
          end
        end
357

358 359 360 361
        context "when a gitlab ci token is provided" do
          let(:build) { create(:ci_build, :running) }
          let(:project) { build.project }
          let(:other_project) { create(:empty_project) }
362

363 364
          context 'when build created by system is authenticated' do
            it "downloads get status 200" do
365 366 367 368 369 370
              clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token

              expect(response).to have_http_status(200)
              expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
            end

371
            it "uploads get status 401 (no project existence information leak)" do
372 373
              push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token

374
              expect(response).to have_http_status(401)
375
            end
376 377 378 379 380 381

            it "downloads from other project get status 404" do
              clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token

              expect(response).to have_http_status(404)
            end
382 383
          end

384 385 386 387 388
          context 'and build created by' do
            before do
              build.update(user: user)
              project.team << [user, :reporter]
            end
389

390 391
            shared_examples 'can download code only' do
              it 'downloads get status 200' do
392 393 394 395 396
                allow_any_instance_of(Repository).
                  to receive(:exists?).and_return(true)

                clone_get "#{project.path_with_namespace}.git",
                  user: 'gitlab-ci-token', password: build.token
397

398 399 400
                expect(response).to have_http_status(200)
                expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
              end
401

402 403 404 405 406 407 408 409 410 411
              it 'downloads from non-existing repository and gets 403' do
                allow_any_instance_of(Repository).
                  to receive(:exists?).and_return(false)

                clone_get "#{project.path_with_namespace}.git",
                  user: 'gitlab-ci-token', password: build.token

                expect(response).to have_http_status(403)
              end

412 413 414 415 416
              it 'uploads get status 403' do
                push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token

                expect(response).to have_http_status(401)
              end
417 418
            end

419 420
            context 'administrator' do
              let(:user) { create(:admin) }
421

422
              it_behaves_like 'can download code only'
423

424 425
              it 'downloads from other project get status 403' do
                clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
426

427 428 429 430 431 432 433 434 435 436 437 438 439 440
                expect(response).to have_http_status(403)
              end
            end

            context 'regular user' do
              let(:user) { create(:user) }

              it_behaves_like 'can download code only'

              it 'downloads from other project get status 404' do
                clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token

                expect(response).to have_http_status(404)
              end
441 442
            end
          end
443
        end
Douwe Maan committed
444 445
      end
    end
Jacob Vosmaer committed
446

447 448 449
    context "when the project path doesn't end in .git" do
      context "GET info/refs" do
        let(:path) { "/#{project.path_with_namespace}/info/refs" }
450

451 452
        context "when no params are added" do
          before { get path }
453

454 455 456
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
          end
457 458
        end

459 460 461
        context "when the upload-pack service is requested" do
          let(:params) { { service: 'git-upload-pack' } }
          before { get path, params }
462

463 464 465
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
          end
466 467
        end

468 469 470
        context "when the receive-pack service is requested" do
          let(:params) { { service: 'git-receive-pack' } }
          before { get path, params }
471

472 473 474
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
          end
475 476
        end

477 478
        context "when the params are anything else" do
          let(:params) { { service: 'git-implode-pack' } }
479
          before { get path, params }
480

481 482
          it "redirects to the sign-in page" do
            expect(response).to redirect_to(new_user_session_path)
483
          end
484 485 486
        end
      end

487 488 489 490
      context "POST git-upload-pack" do
        it "fails to find a route" do
          expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
        end
491 492
      end

493 494 495 496
      context "POST git-receive-pack" do
        it "failes to find a route" do
          expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
        end
497 498 499
      end
    end

500 501
    context "retrieving an info/refs file" do
      before { project.update_attribute(:visibility_level, Project::PUBLIC) }
502

503 504 505 506
      context "when the file exists" do
        before do
          # Provide a dummy file in its place
          allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
507 508
          allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
            Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
509
          end
510

511 512
          get "/#{project.path_with_namespace}/blob/master/info/refs"
        end
513

514 515 516
        it "returns the file" do
          expect(response).to have_http_status(200)
        end
517 518
      end

519 520
      context "when the file does not exist" do
        before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
521

522 523 524
        it "returns not found" do
          expect(response).to have_http_status(404)
        end
525 526 527 528
      end
    end
  end

529 530 531
  describe "User with LDAP identity" do
    let(:user) { create(:omniauth_user, extern_uid: dn) }
    let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
Jacob Vosmaer committed
532

533 534 535 536 537
    before do
      allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
      allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil)
      allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
    end
538

539 540 541 542 543 544 545 546
    context "when authentication fails" do
      context "when no authentication is provided" do
        it "responds with status 401" do
          download('doesnt/exist.git') do |response|
            expect(response).to have_http_status(401)
          end
        end
      end
547

548 549 550 551 552 553 554 555
      context "when username and invalid password are provided" do
        it "responds with status 401" do
          download('doesnt/exist.git', user: user.username, password: "nope") do |response|
            expect(response).to have_http_status(401)
          end
        end
      end
    end
556

557 558 559 560 561 562 563 564
    context "when authentication succeeds" do
      context "when the project doesn't exist" do
        it "responds with status 404" do
          download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
            expect(response).to have_http_status(404)
          end
        end
      end
Jacob Vosmaer committed
565

566 567
      context "when the project exists" do
        let(:project) { create(:project, path: 'project.git-project') }
Jacob Vosmaer committed
568

569 570 571
        before do
          project.team << [user, :master]
        end
Jacob Vosmaer committed
572

573 574 575 576 577 578
        it "responds with status 200" do
          clone_get(path, user: user.username, password: user.password) do |response|
            expect(response).to have_http_status(200)
          end
        end
      end
579 580
    end
  end
Douwe Maan committed
581
end