BigW Consortium Gitlab

git_http_spec.rb 18.5 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 15
  describe "User with no identities" do
    let(:user)    { create(:user) }
    let(:project) { create(:project, 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 60 61
    end

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

63 64 65
      context "when the project is public" do
        before do
          project.update_attribute(:visibility_level, Project::PUBLIC)
Jacob Vosmaer committed
66
        end
67

68 69 70 71 72 73
        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
74

75 76 77
        it "uploads get status 401" do
          upload(path, {}) do |response|
            expect(response).to have_http_status(401)
Jacob Vosmaer committed
78 79
          end
        end
80

81 82
        context "with correct credentials" do
          let(:env) { { user: user.username, password: user.password } }
83

84
          it "uploads get status 403" do
Jacob Vosmaer committed
85
            upload(path, env) do |response|
86
              expect(response).to have_http_status(403)
Jacob Vosmaer committed
87 88 89
            end
          end

90 91 92
          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)
93

94 95 96 97
              upload(path, env) do |response|
                expect(response).to have_http_status(403)
              end
            end
98
          end
Douwe Maan committed
99
        end
100

101 102 103
        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)
104

105 106 107
            download(path, {}) do |response|
              expect(response).to have_http_status(404)
            end
108 109
          end
        end
110

111 112 113 114 115
        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)
116 117
          end
        end
Douwe Maan committed
118 119
      end

120 121 122 123
      context "when the project is private" do
        before do
          project.update_attribute(:visibility_level, Project::PRIVATE)
        end
Douwe Maan committed
124

125 126 127
        context "when no authentication is provided" do
          it "responds with status 401 to downloads" do
            download(path, {}) do |response|
128
              expect(response).to have_http_status(401)
129
            end
Douwe Maan committed
130 131
          end

132 133
          it "responds with status 401 to uploads" do
            upload(path, {}) do |response|
134
              expect(response).to have_http_status(401)
135
            end
Douwe Maan committed
136
          end
137
        end
Douwe Maan committed
138

139 140
        context "when username and password are provided" do
          let(:env) { { user: user.username, password: 'nope' } }
141

142 143 144 145 146
          context "when authentication fails" do
            it "responds with status 401" do
              download(path, env) do |response|
                expect(response).to have_http_status(401)
              end
147
            end
Douwe Maan committed
148

149 150 151 152
            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
153

154 155 156
                clone_get(path, env)

                expect(response).to have_http_status(401)
Douwe Maan committed
157
              end
158
            end
159
          end
Douwe Maan committed
160

161 162
          context "when authentication succeeds" do
            let(:env) { { user: user.username, password: user.password } }
163

164 165 166 167
            context "when the user has access to the project" do
              before do
                project.team << [user, :master]
              end
168

169 170 171 172 173 174 175 176 177
              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
178
              end
179

180 181 182 183 184 185
              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)

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

190 191 192 193 194 195
                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
196 197
              end

198 199 200 201 202
              context "when an oauth token is provided" do
                before do
                  application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
                  @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
                end
203

204 205
                it "downloads get status 200" do
                  clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
206

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

211 212 213 214 215
                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
216 217
              end

218 219 220
              context 'when user has 2FA enabled' do
                let(:user) { create(:user, :two_factor) }
                let(:access_token) { create(:personal_access_token, user: user) }
221

222 223 224
                before do
                  project.team << [user, :master]
                end
225

226 227 228 229 230 231
                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
232 233
                  end

234 235 236 237 238
                  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
239 240 241
                  end
                end

242 243 244 245 246
                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
247 248
                  end

249 250 251 252
                  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
253 254 255 256
                  end
                end
              end

257 258 259 260 261 262
              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
263

264 265 266 267
                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'
268

269 270
                  allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
                  Rack::Attack::Allow2Ban.reset(ip, options)
271

272 273 274
                  maxretry.times.each do
                    expect(attempt_login(false)).to eq(401)
                  end
275

276 277
                  expect(attempt_login(true)).to eq(200)
                  expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
278

279 280 281
                  maxretry.times.each do
                    expect(attempt_login(false)).to eq(401)
                  end
282

283 284
                  Rack::Attack::Allow2Ban.reset(ip, options)
                end
285
              end
Douwe Maan committed
286 287
            end

288 289 290 291 292
            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
293
              end
294

295 296 297 298
              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
299 300
              end
            end
Douwe Maan committed
301 302
          end
        end
303

304 305 306 307
        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) }
308

309
          before do
310
            project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
311
          end
312

313 314
          context 'when build created by system is authenticated' do
            it "downloads get status 200" do
315 316 317 318 319 320
              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

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

324
              expect(response).to have_http_status(401)
325
            end
326 327 328 329 330 331

            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
332 333
          end

334 335 336 337 338
          context 'and build created by' do
            before do
              build.update(user: user)
              project.team << [user, :reporter]
            end
339

340 341 342
            shared_examples 'can download code only' do
              it 'downloads get status 200' do
                clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
343

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

348 349 350 351 352
              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
353 354
            end

355 356
            context 'administrator' do
              let(:user) { create(:admin) }
357

358
              it_behaves_like 'can download code only'
359

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

363 364 365 366 367 368 369 370 371 372 373 374 375 376
                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
377 378
            end
          end
379
        end
Douwe Maan committed
380 381
      end
    end
Jacob Vosmaer committed
382

383 384 385
    context "when the project path doesn't end in .git" do
      context "GET info/refs" do
        let(:path) { "/#{project.path_with_namespace}/info/refs" }
386

387 388
        context "when no params are added" do
          before { get path }
389

390 391 392
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
          end
393 394
        end

395 396 397
        context "when the upload-pack service is requested" do
          let(:params) { { service: 'git-upload-pack' } }
          before { get path, params }
398

399 400 401
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
          end
402 403
        end

404 405 406
        context "when the receive-pack service is requested" do
          let(:params) { { service: 'git-receive-pack' } }
          before { get path, params }
407

408 409 410
          it "redirects to the .git suffix version" do
            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
          end
411 412
        end

413 414
        context "when the params are anything else" do
          let(:params) { { service: 'git-implode-pack' } }
415
          before { get path, params }
416

417 418
          it "redirects to the sign-in page" do
            expect(response).to redirect_to(new_user_session_path)
419
          end
420 421 422
        end
      end

423 424 425 426
      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
427 428
      end

429 430 431 432
      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
433 434 435
      end
    end

436 437
    context "retrieving an info/refs file" do
      before { project.update_attribute(:visibility_level, Project::PUBLIC) }
438

439 440 441 442
      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
443 444
          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')
445
          end
446

447 448
          get "/#{project.path_with_namespace}/blob/master/info/refs"
        end
449

450 451 452
        it "returns the file" do
          expect(response).to have_http_status(200)
        end
453 454
      end

455 456
      context "when the file does not exist" do
        before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
457

458 459 460
        it "returns not found" do
          expect(response).to have_http_status(404)
        end
461 462 463 464
      end
    end
  end

465 466 467
  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
468

469 470 471 472 473
    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
474

475 476 477 478 479 480 481 482
    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
483

484 485 486 487 488 489 490 491
      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
492

493 494 495 496 497 498 499 500
    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
501

502 503
      context "when the project exists" do
        let(:project) { create(:project, path: 'project.git-project') }
Jacob Vosmaer committed
504

505 506 507
        before do
          project.team << [user, :master]
        end
Jacob Vosmaer committed
508

509 510 511 512 513 514
        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
515 516
    end
  end
Douwe Maan committed
517
end