git_http_spec.rb 21.0 KB
Newer Older
D
Douwe Maan 已提交
1 2
require "spec_helper"

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

J
Jacob Vosmaer 已提交
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') }
D
Douwe Maan 已提交
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|
Z
Z.J. van de Weg 已提交
21
            expect(response).to have_http_status(401)
D
Douwe Maan 已提交
22 23
          end
        end
J
Jacob Vosmaer 已提交
24
      end
D
Douwe Maan 已提交
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
D
Douwe Maan 已提交
32 33
          end
        end
S
Stan Hu 已提交
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
S
Stan Hu 已提交
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)
J
Jacob Vosmaer 已提交
48
        project.update_attribute(:visibility_level, Project::PUBLIC)
J
Jacob Vosmaer 已提交
49

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

Z
Z.J. van de Weg 已提交
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
J
Jacob Vosmaer 已提交
57
      end
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

      context 'but the repo is disabled' do
        let(:project) { create(:project, repository_access_level: ProjectFeature::DISABLED, wiki_access_level: ProjectFeature::ENABLED) }
        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" }
J
Jacob Vosmaer 已提交
84

85 86 87
      context "when the project is public" do
        before do
          project.update_attribute(:visibility_level, Project::PUBLIC)
J
Jacob Vosmaer 已提交
88
        end
J
Jacob Vosmaer 已提交
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
J
Jacob Vosmaer 已提交
96

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

103 104
        context "with correct credentials" do
          let(:env) { { user: user.username, password: user.password } }
J
Jacob Vosmaer 已提交
105

106
          it "uploads get status 403" do
J
Jacob Vosmaer 已提交
107
            upload(path, env) do |response|
J
Jacob Vosmaer 已提交
108
              expect(response).to have_http_status(403)
J
Jacob Vosmaer 已提交
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)
J
Jacob Vosmaer 已提交
115

116 117 118 119
              upload(path, env) do |response|
                expect(response).to have_http_status(403)
              end
            end
120
          end
D
Douwe Maan 已提交
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)
J
Jacob Vosmaer 已提交
126

127 128 129
            download(path, {}) do |response|
              expect(response).to have_http_status(404)
            end
130 131
          end
        end
J
Jacob Vosmaer 已提交
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)
J
Jacob Vosmaer 已提交
138 139
          end
        end
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

        context 'when the repo is public' do
          context 'but the repo is disabled' do
            it 'does not allow to clone the repo' do
              project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)

              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
              project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)

              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
              project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)

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

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

179 180 181
        context "when no authentication is provided" do
          it "responds with status 401 to downloads" do
            download(path, {}) do |response|
Z
Z.J. van de Weg 已提交
182
              expect(response).to have_http_status(401)
183
            end
D
Douwe Maan 已提交
184 185
          end

186 187
          it "responds with status 401 to uploads" do
            upload(path, {}) do |response|
Z
Z.J. van de Weg 已提交
188
              expect(response).to have_http_status(401)
189
            end
D
Douwe Maan 已提交
190
          end
J
Jacob Vosmaer 已提交
191
        end
D
Douwe Maan 已提交
192

193 194
        context "when username and password are provided" do
          let(:env) { { user: user.username, password: 'nope' } }
J
Jacob Vosmaer 已提交
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
J
Jacob Vosmaer 已提交
201
            end
D
Douwe Maan 已提交
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')
D
Douwe Maan 已提交
207

208 209 210
                clone_get(path, env)

                expect(response).to have_http_status(401)
D
Douwe Maan 已提交
211
              end
J
Jacob Vosmaer 已提交
212
            end
213
          end
D
Douwe Maan 已提交
214

215 216
          context "when authentication succeeds" do
            let(:env) { { user: user.username, password: user.password } }
J
Jacob Vosmaer 已提交
217

218 219 220 221
            context "when the user has access to the project" do
              before do
                project.team << [user, :master]
              end
J
Jacob Vosmaer 已提交
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
D
Douwe Maan 已提交
232
              end
J
Jacob Vosmaer 已提交
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)

Z
Z.J. van de Weg 已提交
240
                  expect(response).to have_http_status(200)
241
                  expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
J
Jacob Vosmaer 已提交
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
J
Jacob Vosmaer 已提交
336

337 338
                  Rack::Attack::Allow2Ban.reset(ip, options)
                end
339
              end
D
Douwe Maan 已提交
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
D
Douwe Maan 已提交
347
              end
J
Jacob Vosmaer 已提交
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
J
Jacob Vosmaer 已提交
353 354
              end
            end
D
Douwe Maan 已提交
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) }
J
Jacob Vosmaer 已提交
362

363
          before do
364
            project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
365
          end
J
Jacob Vosmaer 已提交
366

367 368
          context 'when build created by system is authenticated' do
            it "downloads get status 200" do
369 370 371 372 373 374
              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

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

378
              expect(response).to have_http_status(401)
379
            end
380 381 382 383 384 385

            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
386 387
          end

388 389 390 391 392
          context 'and build created by' do
            before do
              build.update(user: user)
              project.team << [user, :reporter]
            end
393

394 395
            shared_examples 'can download code only' do
              it 'downloads get status 200' do
396 397 398 399 400
                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
401

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

406 407 408 409 410 411 412 413 414 415
              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

416 417 418 419 420
              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
421 422
            end

423 424
            context 'administrator' do
              let(:user) { create(:admin) }
425

426
              it_behaves_like 'can download code only'
427

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

431 432 433 434 435 436 437 438 439 440 441 442 443 444
                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
445 446
            end
          end
J
Jacob Vosmaer 已提交
447
        end
D
Douwe Maan 已提交
448 449
      end
    end
J
Jacob Vosmaer 已提交
450

451 452 453
    context "when the project path doesn't end in .git" do
      context "GET info/refs" do
        let(:path) { "/#{project.path_with_namespace}/info/refs" }
454

455 456
        context "when no params are added" do
          before { get path }
457

458 459 460
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
          end
461 462
        end

463 464 465
        context "when the upload-pack service is requested" do
          let(:params) { { service: 'git-upload-pack' } }
          before { get path, params }
466

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

472 473 474
        context "when the receive-pack service is requested" do
          let(:params) { { service: 'git-receive-pack' } }
          before { get path, params }
475

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

481 482
        context "when the params are anything else" do
          let(:params) { { service: 'git-implode-pack' } }
483
          before { get path, params }
484

485 486
          it "redirects to the sign-in page" do
            expect(response).to redirect_to(new_user_session_path)
487
          end
488 489 490
        end
      end

491 492 493 494
      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
495 496
      end

497 498 499 500
      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
501 502 503
      end
    end

504 505
    context "retrieving an info/refs file" do
      before { project.update_attribute(:visibility_level, Project::PUBLIC) }
506

507 508 509 510
      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
511 512
          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')
513
          end
514

515 516
          get "/#{project.path_with_namespace}/blob/master/info/refs"
        end
517

518 519 520
        it "returns the file" do
          expect(response).to have_http_status(200)
        end
521 522
      end

523 524
      context "when the file does not exist" do
        before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
525

526 527 528
        it "returns not found" do
          expect(response).to have_http_status(404)
        end
529 530 531 532
      end
    end
  end

533 534 535
  describe "User with LDAP identity" do
    let(:user) { create(:omniauth_user, extern_uid: dn) }
    let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
J
Jacob Vosmaer 已提交
536

537 538 539 540 541
    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
542

543 544 545 546 547 548 549 550
    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
J
Jacob Vosmaer 已提交
551

552 553 554 555 556 557 558 559
      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
J
Jacob Vosmaer 已提交
560

561 562 563 564 565 566 567 568
    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
J
Jacob Vosmaer 已提交
569

570 571
      context "when the project exists" do
        let(:project) { create(:project, path: 'project.git-project') }
J
Jacob Vosmaer 已提交
572

573 574 575
        before do
          project.team << [user, :master]
        end
J
Jacob Vosmaer 已提交
576

577 578 579 580 581 582
        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
583 584
    end
  end
D
Douwe Maan 已提交
585
end