BigW Consortium Gitlab

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

3
describe 'Git HTTP requests', lib: true do
Douwe Maan committed
4
  let(:user)    { create(:user) }
5
  let(:project) { create(:project, path: 'project.git-project') }
Douwe Maan committed
6

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
  context "when the project doesn't exist" do
    context "when no authentication is provided" do
15
      it "responds with status 401 (no project existence information leak)" do
16
        download('doesnt/exist.git') do |response|
17
          expect(response).to have_http_status(401)
Douwe Maan committed
18 19
        end
      end
20
    end
Douwe Maan committed
21

22 23 24 25
    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|
26
            expect(response).to have_http_status(401)
Douwe Maan committed
27 28
          end
        end
29
      end
Douwe Maan committed
30

31 32 33
      context "when authentication succeeds" do
        it "responds with status 404" do
          download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
34
            expect(response).to have_http_status(404)
Douwe Maan committed
35 36 37 38
          end
        end
      end
    end
39
  end
Douwe Maan committed
40

41 42 43 44
  context "when the Wiki for a project exists" do
    it "responds with the right project" do
      wiki = ProjectWiki.new(project)
      project.update_attribute(:visibility_level, Project::PUBLIC)
45

46 47
      download("/#{wiki.repository.path_with_namespace}.git") do |response|
        json_body = ActiveSupport::JSON.decode(response.body)
48

49
        expect(response).to have_http_status(200)
50
        expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
51 52
      end
    end
53
  end
54

55 56
  context "when the project exists" do
    let(:path) { "#{project.path_with_namespace}.git" }
Douwe Maan committed
57

58 59 60 61
    context "when the project is public" do
      before do
        project.update_attribute(:visibility_level, Project::PUBLIC)
      end
Jacob Vosmaer committed
62

Jacob Vosmaer committed
63
      it "downloads get status 200" do
Jacob Vosmaer committed
64
        download(path, {}) do |response|
65
          expect(response).to have_http_status(200)
66
        end
67
      end
Jacob Vosmaer committed
68

Jacob Vosmaer committed
69
      it "uploads get status 401" do
Jacob Vosmaer committed
70
        upload(path, {}) do |response|
71
          expect(response).to have_http_status(401)
Jacob Vosmaer committed
72 73
        end
      end
74

Jacob Vosmaer committed
75 76 77
      context "with correct credentials" do
        let(:env) { { user: user.username, password: user.password } }

78
        it "uploads get status 403" do
Jacob Vosmaer committed
79
          upload(path, env) do |response|
80
            expect(response).to have_http_status(403)
Jacob Vosmaer committed
81 82
          end
        end
83

Jacob Vosmaer committed
84 85 86
        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)
87

Jacob Vosmaer committed
88
            upload(path, env) do |response|
89
              expect(response).to have_http_status(403)
Jacob Vosmaer committed
90 91 92 93 94
            end
          end
        end
      end

95 96 97 98
      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)

Jacob Vosmaer committed
99
          download(path, {}) do |response|
100
            expect(response).to have_http_status(404)
101
          end
Douwe Maan committed
102
        end
103 104
      end
    end
105

106 107 108 109 110 111
    context "when the project is private" do
      before do
        project.update_attribute(:visibility_level, Project::PRIVATE)
      end

      context "when no authentication is provided" do
112
        it "responds with status 401 to downloads" do
Jacob Vosmaer committed
113
          download(path, {}) do |response|
114
            expect(response).to have_http_status(401)
115 116
          end
        end
117 118

        it "responds with status 401 to uploads" do
Jacob Vosmaer committed
119
          upload(path, {}) do |response|
120
            expect(response).to have_http_status(401)
121 122
          end
        end
Douwe Maan committed
123 124
      end

125 126
      context "when username and password are provided" do
        let(:env) { { user: user.username, password: 'nope' } }
Douwe Maan committed
127

128
        context "when authentication fails" do
Douwe Maan committed
129
          it "responds with status 401" do
130
            download(path, env) do |response|
131
              expect(response).to have_http_status(401)
132
            end
Douwe Maan committed
133 134
          end

135
          context "when the user is IP banned" do
Douwe Maan committed
136
            it "responds with status 401" do
137 138
              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')
139

140
              clone_get(path, env)
141

142
              expect(response).to have_http_status(401)
143
            end
Douwe Maan committed
144
          end
145
        end
Douwe Maan committed
146

147 148
        context "when authentication succeeds" do
          let(:env) { { user: user.username, password: user.password } }
149

150 151 152 153
          context "when the user has access to the project" do
            before do
              project.team << [user, :master]
            end
Douwe Maan committed
154

155 156 157 158
            context "when the user is blocked" do
              it "responds with status 404" do
                user.block
                project.team << [user, :master]
Douwe Maan committed
159

160
                download(path, env) do |response|
161
                  expect(response).to have_http_status(404)
Douwe Maan committed
162 163
                end
              end
164
            end
Douwe Maan committed
165

166
            context "when the user isn't blocked" do
167
              it "downloads get status 200" do
168
                expect(Rack::Attack::Allow2Ban).to receive(:reset)
169

170
                clone_get(path, env)
171

172
                expect(response).to have_http_status(200)
Douwe Maan committed
173
              end
174

Jacob Vosmaer committed
175 176
              it "uploads get status 200" do
                upload(path, env) do |response|
177
                  expect(response).to have_http_status(200)
178
                end
Jacob Vosmaer committed
179
              end
180
            end
181

182 183 184 185 186 187 188 189 190
            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

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

191
                expect(response).to have_http_status(200)
192 193 194 195 196
              end

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

197
                expect(response).to have_http_status(401)
198 199 200
              end
            end

201 202 203 204 205 206
            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
207

208 209 210 211
              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'
212

213 214
                allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
                Rack::Attack::Allow2Ban.reset(ip, options)
215

216 217 218
                maxretry.times.each do
                  expect(attempt_login(false)).to eq(401)
                end
219

220 221
                expect(attempt_login(true)).to eq(200)
                expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
222

223 224
                maxretry.times.each do
                  expect(attempt_login(false)).to eq(401)
225
                end
226 227

                Rack::Attack::Allow2Ban.reset(ip, options)
228
              end
Douwe Maan committed
229
            end
230
          end
Douwe Maan committed
231

232
          context "when the user doesn't have access to the project" do
Jacob Vosmaer committed
233
            it "downloads get status 404" do
234
              download(path, user: user.username, password: user.password) do |response|
235
                expect(response).to have_http_status(404)
Douwe Maan committed
236 237
              end
            end
238

239
            it "uploads get status 404" do
Jacob Vosmaer committed
240
              upload(path, user: user.username, password: user.password) do |response|
241
                expect(response).to have_http_status(404)
Jacob Vosmaer committed
242 243
              end
            end
Douwe Maan committed
244 245
          end
        end
246
      end
Douwe Maan committed
247

248
      context "when a gitlab ci token is provided" do
249 250 251 252
        let(:token) { 123 }
        let(:project) { FactoryGirl.create :empty_project }

        before do
253
          project.update_attributes(runners_token: token, builds_enabled: true)
254
        end
Douwe Maan committed
255

256
        it "downloads get status 200" do
257
          clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
Douwe Maan committed
258

259
          expect(response).to have_http_status(200)
Douwe Maan committed
260
        end
261 262 263 264

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

265
          expect(response).to have_http_status(401)
266
        end
Douwe Maan committed
267 268 269
      end
    end
  end
Jacob Vosmaer committed
270

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  context "when the project path doesn't end in .git" do
    context "GET info/refs" do
      let(:path) { "/#{project.path_with_namespace}/info/refs" }

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

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

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

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

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

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

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

        it "redirects to the sign-in page" do
          expect(response).to redirect_to(new_user_session_path)
        end
      end
    end

    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
    end

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

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
  context "retrieving an info/refs file" do
    before { project.update_attribute(:visibility_level, Project::PUBLIC) }

    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
        allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
          Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
        end

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

      it "returns the file" do
339
        expect(response).to have_http_status(200)
340 341 342
      end
    end

343
    context "when the file does not exist" do
344 345 346
      before { get "/#{project.path_with_namespace}/blob/master/info/refs" }

      it "returns not found" do
347
        expect(response).to have_http_status(404)
348 349 350 351
      end
    end
  end

352
  def clone_get(project, options = {})
353
    get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
354
  end
355

356
  def clone_post(project, options = {})
357
    post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
358 359
  end

360
  def push_get(project, options = {})
361
    get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
Jacob Vosmaer committed
362 363
  end

364
  def push_post(project, options = {})
365
    post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
Jacob Vosmaer committed
366 367
  end

368 369
  def download(project, user: nil, password: nil, spnego_request_token: nil)
    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
370

Jacob Vosmaer committed
371
    clone_get(*args)
372
    yield response
373

Jacob Vosmaer committed
374
    clone_post(*args)
375 376
    yield response
  end
377

378 379
  def upload(project, user: nil, password: nil, spnego_request_token: nil)
    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
Jacob Vosmaer committed
380

Jacob Vosmaer committed
381
    push_get(*args)
Jacob Vosmaer committed
382 383
    yield response

Jacob Vosmaer committed
384
    push_post(*args)
Jacob Vosmaer committed
385 386 387
    yield response
  end

388 389
  def auth_env(user, password, spnego_request_token)
    env = {}
390
    if user && password
391 392 393
      env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
    elsif spnego_request_token
      env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
394
    end
395 396

    env
397
  end
Douwe Maan committed
398
end