Commit e08eba18 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d4633b0e
......@@ -288,9 +288,7 @@ class ApplicationController < ActionController::Base
def check_password_expiration
return if session[:impersonator_id] || !current_user&.allow_password_authentication?
password_expires_at = current_user&.password_expires_at
if password_expires_at && password_expires_at < Time.now
if current_user&.password_expired?
return redirect_to new_profile_password_path
end
end
......
......@@ -2,6 +2,7 @@
class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
......@@ -81,7 +82,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
header: {
Authorization: request.headers['Authorization']
Authorization: authorization_header
}.compact
}
}
......@@ -92,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: {
Authorization: request.headers['Authorization'],
Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE
......@@ -122,6 +123,18 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
def authorization_header
strong_memoize(:authorization_header) do
lfs_auth_header || request.headers['Authorization']
end
end
def lfs_auth_header
return unless user.is_a?(User)
Gitlab::LfsToken.new(user).basic_encoding
end
end
Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController')
......@@ -53,7 +53,7 @@ module Ci
pages_config.enabled &&
pages_config.artifacts_server &&
EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
job.project.public?
(pages_config.access_control || job.project.public?)
end
private
......
......@@ -1519,6 +1519,10 @@ class User < ApplicationRecord
todos.find_by(target: target, state: :pending)
end
def password_expired?
!!(password_expires_at && password_expires_at < Time.now)
end
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......
---
title: Generate LFS token authorization for user LFS requests
merge_request: 17332
author:
type: fixed
---
title: Enable preview of private artifacts
merge_request: 16675
author: Tuomo Ala-Vannesluoma
type: added
......@@ -25,17 +25,17 @@ solution should balance the costs against the benefits.
There are many options when choosing a highly-available GitLab architecture. We
recommend engaging with GitLab Support to choose the best architecture for your
use-case. This page contains some various options and guidelines based on
use case. This page contains some various options and guidelines based on
experience with GitLab.com and Enterprise Edition on-premises customers.
For a detailed insight into how GitLab scales and configures GitLab.com, you can
For detailed insight into how GitLab scales and configures GitLab.com, you can
watch [this 1 hour Q&A](https://www.youtube.com/watch?v=uCU8jdYzpac)
with [John Northrup](https://gitlab.com/northrup), and live questions coming in from some of our customers.
## GitLab Components
The following components need to be considered for a scaled or highly-available
environment. In many cases components can be combined on the same nodes to reduce
environment. In many cases, components can be combined on the same nodes to reduce
complexity.
- Unicorn/Workhorse - Web-requests (UI, API, Git over HTTP)
......@@ -57,12 +57,12 @@ infrastructure and maintenance costs of full high availability.
### Basic Scaling
This is the simplest form of scaling and will work for the majority of
cases. Backend components such as PostgreSQL, Redis and storage are offloaded
cases. Backend components such as PostgreSQL, Redis, and storage are offloaded
to their own nodes while the remaining GitLab components all run on 2 or more
application nodes.
This form of scaling also works well in a cloud environment when it is more
cost-effective to deploy several small nodes rather than a single
cost effective to deploy several small nodes rather than a single
larger one.
- 1 PostgreSQL node
......@@ -85,11 +85,11 @@ you can continue with the next step.
### Full Scaling
For very large installations it may be necessary to further split components
for maximum scalability. In a fully-scaled architecture the application node
For very large installations, it might be necessary to further split components
for maximum scalability. In a fully-scaled architecture, the application node
is split into separate Sidekiq and Unicorn/Workhorse nodes. One indication that
this architecture is required is if Sidekiq queues begin to periodically increase
in size, indicating that there is contention or not enough resources.
in size, indicating that there is contention or there are not enough resources.
- 1 PostgreSQL node
- 1 Redis node
......@@ -100,7 +100,7 @@ in size, indicating that there is contention or not enough resources.
## High Availability Architecture Examples
When organizations require scaling *and* high availability the following
When organizations require scaling *and* high availability, the following
architectures can be utilized. As the introduction section at the top of this
page mentions, there is a tradeoff between cost/complexity and uptime. Be sure
this complexity is absolutely required before taking the step into full
......@@ -108,11 +108,11 @@ high availability.
For all examples below, we recommend running Consul and Redis Sentinel on
dedicated nodes. If Consul is running on PostgreSQL nodes or Sentinel on
Redis nodes there is a potential that high resource usage by PostgreSQL or
Redis nodes, there is a potential that high resource usage by PostgreSQL or
Redis could prevent communication between the other Consul and Sentinel nodes.
This may lead to the other nodes believing a failure has occurred and automated
failover is necessary. Isolating them from the services they monitor reduces
the chances of split-brain.
This may lead to the other nodes believing a failure has occurred and initiating
automated failover. Isolating Redis and Consul from the services they monitor
reduces the chances of a false positive that a failure has occurred.
The examples below do not really address high availability of NFS. Some enterprises
have access to NFS appliances that manage availability. This is the best case
......@@ -131,7 +131,7 @@ trade-offs and limits.
This architecture will work well for many GitLab customers. Larger customers
may begin to notice certain events cause contention/high load - for example,
cloning many large repositories with binary files, high API usage, a large
number of enqueued Sidekiq jobs, etc. If this happens you should consider
number of enqueued Sidekiq jobs, and so on. If this happens, you should consider
moving to a hybrid or fully distributed architecture depending on what is causing
the contention.
......@@ -162,32 +162,11 @@ contention due to certain workloads.
![Hybrid architecture diagram](img/hybrid.png)
#### Reference Architecture
- **Supported Users (approximate):** 10,000
- **Known Issues:** While validating the reference architecture, slow endpoints were discovered and are being investigated. [See issue #64335](https://gitlab.com/gitlab-org/gitlab-foss/issues/64335)
The Support and Quality teams built, performance tested, and validated an
environment that supports about 10,000 users. The specifications below are a
representation of the work so far. The specifications may be adjusted in the
future based on additional testing and iteration.
NOTE: **Note:** The specifications here were performance tested against a specific coded workload. Your exact needs may be more, depending on your workload. Your workload is influenced by factors such as - but not limited to - how active your users are, how much automation you use, mirroring, and repo/change size.
- 3 PostgreSQL - 4 CPU, 16GiB memory per node
- 1 PgBouncer - 2 CPU, 4GiB memory
- 2 Redis - 2 CPU, 8GiB memory per node
- 3 Consul/Sentinel - 2 CPU, 2GiB memory per node
- 4 Sidekiq - 4 CPU, 16GiB memory per node
- 5 GitLab application nodes - 16 CPU, 64GiB memory per node
- 1 Gitaly - 16 CPU, 64GiB memory
- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage
### Fully Distributed
This architecture scales to hundreds of thousands of users and projects and is
the basis of the GitLab.com architecture. While this scales well it also comes
with the added complexity of many more nodes to configure, manage and monitor.
with the added complexity of many more nodes to configure, manage, and monitor.
- 3 PostgreSQL nodes
- 4 or more Redis nodes (2 separate clusters for persistent and cache data)
......@@ -214,3 +193,59 @@ separately:
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
1. [Monitoring node (Prometheus and Grafana)](monitoring_node.md)
## Reference Architecture Examples
These reference architecture examples rely on the general rule that approximately 2 requests per second (RPS) of load is generated for every 100 users.
### 10,000 User Configuration
- **Supported Users (approximate):** 10,000
- **RPS:** 200 requests per second
- **Known Issues:** While validating the reference architecture, slow endpoints were discovered and are being investigated. [gitlab-org/gitlab-ce/issues/64335](https://gitlab.com/gitlab-org/gitlab-ce/issues/64335)
The Support and Quality teams built, performance tested, and validated an
environment that supports about 10,000 users. The specifications below are a
representation of the work so far. The specifications may be adjusted in the
future based on additional testing and iteration.
NOTE: **Note:** The specifications here were performance tested against a specific coded workload. Your exact needs may be more, depending on your workload. Your workload is influenced by factors such as - but not limited to - how active your users are, how much automation you use, mirroring, and repo/change size.
- 3 PostgreSQL - 4 CPU, 16GiB memory per node
- 1 PgBouncer - 2 CPU, 4GiB memory
- 2 Redis - 2 CPU, 8GiB memory per node
- 3 Consul/Sentinel - 2 CPU, 2GiB memory per node
- 4 Sidekiq - 4 CPU, 16GiB memory per node
- 5 GitLab application nodes - 16 CPU, 64GiB memory per node
- 1 Gitaly - 16 CPU, 64GiB memory
- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage
### 25,000 User Configuration
- **Supported Users (approximate):** 25,000
- **RPS:** 500 requests per second
- **Status:** Work-in-progress
- **Related Issues:** [gitlab-org/quality/performance/issues/57](https://gitlab.com/gitlab-org/quality/performance/issues/57)
The Support and Quality teams are in the process of building and performance testing
an environment that will support about 25,000 users. The specifications below
are a work-in-progress representation of the work so far. The Quality team will be
certifying this environment in late 2019. The specifications may be adjusted
prior to certification based on performance testing.
TBD: Add specs
### 50,000 User Configuration
- **Supported Users (approximate):** 50,000
- **RPS:** 1,000 requests per second
- **Status:** Work-in-progress
- **Related Issues:** [gitlab-org/quality/performance/issues/66](https://gitlab.com/gitlab-org/quality/performance/issues/66)
The Support and Quality teams are in the process of building and performance testing
an environment that will support about 50,000 users. The specifications below
are a work-in-progress representation of the work so far. The Quality team will be
certifying this environment in late 2019. The specifications may be adjusted
prior to certification based on performance testing.
TBD: Add specs
......@@ -56,6 +56,8 @@ For more examples on artifacts, follow the [artifacts reference in
> directly in a new tab without the need to download them when
> [GitLab Pages](../../../administration/pages/index.md) is enabled.
> The same holds for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
> With [GitLab 12.4][gitlab-16675], also artifacts in private projects can be previewed
> when [GitLab Pages access control](../../../administration/pages/index.md#access-control) is enabled.
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
......@@ -198,6 +200,7 @@ In order to retrieve a job artifact of a different project, you might need to us
[expiry date]: ../../../ci/yaml/README.md#artifactsexpire_in
[ce-14399]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14399
[gitlab-16675]: https://gitlab.com/gitlab-org/gitlab/merge_requests/16675
<!-- ## Troubleshooting
......
......@@ -231,7 +231,7 @@ module Gitlab
authentication_abilities =
if token_handler.user?
full_authentication_abilities
read_write_project_authentication_abilities
elsif token_handler.deploy_key_pushable?(project)
read_write_authentication_abilities
else
......@@ -272,10 +272,21 @@ module Gitlab
]
end
def read_only_authentication_abilities
def read_only_project_authentication_abilities
[
:read_project,
:download_code,
:download_code
]
end
def read_write_project_authentication_abilities
read_only_project_authentication_abilities + [
:push_code
]
end
def read_only_authentication_abilities
read_only_project_authentication_abilities + [
:read_container_image
]
end
......
......@@ -34,8 +34,11 @@ module Gitlab
HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME)
end
# When the token is an lfs one and the actor
# is blocked or the password has been changed,
# the token is no longer valid
def token_valid?(token_to_check)
HMACToken.new(actor).token_valid?(token_to_check)
HMACToken.new(actor).token_valid?(token_to_check) && valid_user?
end
def deploy_key_pushable?(project)
......@@ -46,6 +49,12 @@ module Gitlab
user? ? :lfs_token : :lfs_deploy_token
end
def valid_user?
return true unless user?
!actor.blocked? && (!actor.allow_password_authentication? || !actor.password_expired?)
end
def authentication_payload(repository_http_path)
{
username: actor_name,
......@@ -55,6 +64,10 @@ module Gitlab
}
end
def basic_encoding
ActionController::HttpAuthentication::Basic.encode_credentials(actor_name, token)
end
private # rubocop:disable Lint/UselessAccessModifier
class HMACToken
......
......@@ -286,6 +286,25 @@ describe Projects::ArtifactsController do
expect(response).to render_template('projects/artifacts/file')
end
end
context 'when the project is private and pages access control is enabled' do
let(:private_project) { create(:project, :repository, :private) }
let(:pipeline) { create(:ci_pipeline, project: private_project) }
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
private_project.add_developer(user)
allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
end
it 'renders the file view' do
get :file, params: { namespace_id: private_project.namespace, project_id: private_project, job_id: job, path: 'ci_artifacts.txt' }
expect(response).to have_gitlab_http_status(302)
end
end
end
describe 'GET raw' do
......
......@@ -114,5 +114,24 @@ describe "User browses artifacts" do
it { expect(page).to have_link("doc_sample.txt").and have_no_selector(".js-artifact-tree-external-icon") }
end
context "when the project is private and pages access control is enabled" do
let!(:private_project) { create(:project, :private) }
let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
let(:user) { create(:user) }
before do
private_project.add_developer(user)
allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
sign_in(user)
visit(browse_project_job_artifacts_path(private_project, job, "other_artifacts_0.1.2"))
end
it { expect(page).to have_link("doc_sample.txt").and have_selector(".js-artifact-tree-external-icon") }
end
end
end
......@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Auth do
let(:gl_auth) { described_class }
set(:project) { create(:project) }
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
......@@ -90,13 +91,13 @@ describe Gitlab::Auth do
end
it 'recognises user-less build' do
expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, described_class.build_authentication_abilities))
end
it 'recognises user token' do
build.update(user: create(:user))
expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, described_class.build_authentication_abilities))
end
end
......@@ -117,26 +118,25 @@ describe Gitlab::Auth do
end
it 'recognizes other ci services' do
project = create(:project)
project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, described_class.build_authentication_abilities))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
include_examples 'user login operation with unique ip limit' do
let(:user) { create(:user, password: 'password') }
def operation
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
end
......@@ -146,7 +146,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, described_class.read_write_project_authentication_abilities))
end
it 'recognizes deploy key lfs tokens' do
......@@ -154,7 +154,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
end
it 'does not try password auth before oauth' do
......@@ -167,22 +167,20 @@ describe Gitlab::Auth do
end
it 'grants deploy key write permissions' do
project = create(:project)
key = create(:deploy_key)
create(:deploy_keys_project, :write_access, deploy_key: key, project: project)
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_write_authentication_abilities))
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_write_authentication_abilities))
end
it 'does not grant deploy key write permissions' do
project = create(:project)
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
end
end
......@@ -193,7 +191,7 @@ describe Gitlab::Auth do
it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities))
expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, described_class.full_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
......@@ -214,7 +212,7 @@ describe Gitlab::Auth do
it 'succeeds for personal access tokens with the `api` scope' do
personal_access_token = create(:personal_access_token, scopes: ['api'])
expect_results_with_abilities(personal_access_token, full_authentication_abilities)
expect_results_with_abilities(personal_access_token, described_class.full_authentication_abilities)
end
it 'succeeds for personal access tokens with the `read_repository` scope' do
......@@ -244,7 +242,7 @@ describe Gitlab::Auth do
it 'succeeds if it is an impersonation token' do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect_results_with_abilities(impersonation_token, full_authentication_abilities)
expect_results_with_abilities(impersonation_token, described_class.full_authentication_abilities)
end
it 'limits abilities based on scope' do
......@@ -267,7 +265,7 @@ describe Gitlab::Auth do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
.to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
.to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
it 'fails through oauth authentication when the username is oauth2' do
......@@ -278,7 +276,7 @@ describe Gitlab::Auth do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
.to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
.to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
end
......@@ -296,7 +294,6 @@ describe Gitlab::Auth do
end
context 'while using deploy tokens' do
let(:project) { create(:project) }
let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) }
context 'when deploy token and user have the same username' do
......@@ -316,7 +313,7 @@ describe Gitlab::Auth do
end
it 'succeeds for the user' do
auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities)
expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip'))
.to eq(auth_success)
......@@ -344,7 +341,7 @@ describe Gitlab::Auth do
end
context 'and belong to different projects' do
let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [create(:project)]) }
let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) }
let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [project]) }
it 'succeeds for the right token' do
......@@ -582,37 +579,6 @@ describe Gitlab::Auth do
private
def build_authentication_abilities
[
:read_project,
:build_download_code,
:build_read_container_image,
:build_create_container_image,
:build_destroy_container_image
]
end
def read_only_authentication_abilities
[
:read_project,
:download_code,
:read_container_image
]
end
def read_write_authentication_abilities
read_only_authentication_abilities + [
:push_code,
:create_container_image
]
end
def full_authentication_abilities
read_write_authentication_abilities + [
:admin_container_image
]
end
def expect_results_with_abilities(personal_access_token, abilities, success = true)
expect(gl_auth).to receive(:rate_limit!).with('ip', success: success, login: '')
expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
......
......@@ -115,6 +115,46 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end
end
context 'when the actor is a regular user' do
context 'when the user is blocked' do
let(:actor) { create(:user, :blocked) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
context 'when the user password is expired' do
let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
end
context 'when the actor is an ldap user' do
before do
allow(actor).to receive(:ldap_user?).and_return(true)
end
context 'when the user is blocked' do
let(:actor) { create(:user, :blocked) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
context 'when the user password is expired' do
let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns true' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end
end
end
end
end
......
......@@ -3616,4 +3616,34 @@ describe User do
end
end
end
describe '#password_expired?' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired? }
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
it 'returns true' do
is_expected.to be_truthy
end
end
context 'when password_expires_at is in the future' do
let(:password_expires_at) { 1.minute.from_now }
it 'returns false' do
is_expected.to be_falsey
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Git LFS API and storage' do
include WorkhorseHelpers
include LfsHttpHelpers
include ProjectForksHelper
let(:user) { create(:user) }
set(:project) { create(:project, :repository) }
set(:other_project) { create(:project, :repository) }
set(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do
......@@ -19,201 +22,163 @@ describe 'Git LFS API and storage' do
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
let(:sample_object) { { 'oid' => sample_oid, 'size' => sample_size } }
let(:non_existing_object_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
let(:non_existing_object_size) { 1575078 }
let(:non_existing_object) { { 'oid' => non_existing_object_oid, 'size' => non_existing_object_size } }
let(:multiple_objects) { [sample_object, non_existing_object] }
describe 'when lfs is disabled' do
let(:project) { create(:project) }
let(:body) do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 },
{ 'oid' => sample_oid,
'size' => sample_size }
],
'operation' => 'upload'
}
end
let(:lfs_enabled) { true }
before do
stub_lfs_setting(enabled: lfs_enabled)
end
describe 'when LFS is disabled' do
let(:lfs_enabled) { false }
let(:body) { upload_body(multiple_objects) }
let(:authorization) { authorize_user }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
post_lfs_json batch_url(project), body, headers
end
it 'responds with 501' do
expect(response).to have_gitlab_http_status(501)
expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.')
end
it_behaves_like 'LFS http 501 response'
end
context 'project specific LFS settings' do
let(:project) { create(:project) }
let(:body) do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 },
{ 'oid' => sample_oid,
'size' => sample_size }
],
'operation' => 'upload'
}
end
let(:body) { upload_body(sample_object) }
let(:authorization) { authorize_user }
before do
project.add_maintainer(user)
project.update_attribute(:lfs_enabled, project_lfs_enabled)
subject
end
context 'with LFS disabled globally' do
before do
project.add_maintainer(user)
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
end
let(:lfs_enabled) { false }
describe 'LFS disabled in project' do
before do
project.update_attribute(:lfs_enabled, false)
end
let(:project_lfs_enabled) { false }
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
expect(response).to have_gitlab_http_status(501)
it_behaves_like 'LFS http 501 response'
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
expect(response).to have_gitlab_http_status(501)
it_behaves_like 'LFS http 501 response'
end
end
describe 'LFS enabled in project' do
before do
project.update_attribute(:lfs_enabled, true)
end
let(:project_lfs_enabled) { true }
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
expect(response).to have_gitlab_http_status(501)
it_behaves_like 'LFS http 501 response'
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
expect(response).to have_gitlab_http_status(501)
it_behaves_like 'LFS http 501 response'
end
end
end
context 'with LFS enabled globally' do
before do
project.add_maintainer(user)
enable_lfs
end
describe 'LFS disabled in project' do
before do
project.update_attribute(:lfs_enabled, false)
end
let(:project_lfs_enabled) { false }
it 'responds with a 403 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
it_behaves_like 'LFS http 403 response'
end
it 'responds with a 403 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
it_behaves_like 'LFS http 403 response'
end
end
describe 'LFS enabled in project' do
before do
project.update_attribute(:lfs_enabled, true)
end
let(:project_lfs_enabled) { true }
it 'responds with a 200 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
expect(response).to have_gitlab_http_status(200)
expect(json_response['objects'].first['size']).to eq(1575078)
it_behaves_like 'LFS http 200 response'
end
it 'responds with a 200 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
expect(response).to have_gitlab_http_status(200)
it_behaves_like 'LFS http 200 response'
end
end
end
end
describe 'deprecated API' do
let(:project) { create(:project) }
before do
enable_lfs
end
let(:authorization) { authorize_user }
shared_examples 'a deprecated' do
it 'responds with 501' do
expect(response).to have_gitlab_http_status(501)
shared_examples 'deprecated request' do
before do
subject
end
it 'returns deprecated message' do
expect(json_response).to include('message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.')
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 }
let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
end
end
context 'when fetching lfs object using deprecated API' do
let(:authorization) { authorize_user }
before do
get "#{project.http_url_to_repo}/info/lfs/objects/#{sample_oid}", params: {}, headers: headers
end
context 'when fetching LFS object using deprecated API' do
subject { get(deprecated_objects_url(project, sample_oid), params: {}, headers: headers) }
it_behaves_like 'a deprecated'
it_behaves_like 'deprecated request'
end
context 'when handling lfs request using deprecated API' do
let(:authorization) { authorize_user }
before do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
end
context 'when handling LFS request using deprecated API' do
subject { post_lfs_json(deprecated_objects_url(project), nil, headers) }
it_behaves_like 'a deprecated'
it_behaves_like 'deprecated request'
end
def deprecated_objects_url(project, oid = nil)
File.join(["#{project.http_url_to_repo}/info/lfs/objects/", oid].compact)
end
end
describe 'when fetching lfs object' do
let(:project) { create(:project) }
describe 'when fetching LFS object' do
let(:update_permissions) { }
let(:before_get) { }
before do
enable_lfs
update_permissions
before_get
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
get objects_url(project, sample_oid), params: {}, headers: headers
end
context 'and request comes from gitlab-workhorse' do
context 'without user being authorized' do
it 'responds with status 401' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
context 'with required headers' do
shared_examples 'responds with a file' do
let(:sendfile) { 'X-Sendfile' }
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with the file location' do
expect(response.headers['Content-Type']).to eq('application/octet-stream')
......@@ -229,9 +194,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
it 'responds with status 404' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
context 'and does have project access' do
......@@ -249,9 +212,7 @@ describe 'Git LFS API and storage' do
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'responds with redirect' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with the workhorse send-url' do
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
......@@ -288,7 +249,7 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
end
describe 'when using a user key' do
describe 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
context 'when user allowed' do
......@@ -298,6 +259,18 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'responds with a file'
context 'when user password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
it_behaves_like 'LFS http 401 response'
end
context 'when user is blocked' do
let(:user) { create(:user, :blocked)}
it_behaves_like 'LFS http 401 response'
end
end
context 'when user not allowed' do
......@@ -305,9 +278,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
it 'responds with status 404' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
end
......@@ -337,7 +308,6 @@ describe 'Git LFS API and storage' do
end
context 'for other project' do
let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:update_permissions) do
......@@ -361,7 +331,6 @@ describe 'Git LFS API and storage' do
end
context 'regular user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
......@@ -384,166 +353,147 @@ describe 'Git LFS API and storage' do
context 'without required headers' do
let(:authorization) { authorize_user }
it 'responds with status 404' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
end
end
describe 'when handling lfs batch request' do
describe 'when handling LFS batch request' do
let(:update_lfs_permissions) { }
let(:update_user_permissions) { }
before do
enable_lfs
update_lfs_permissions
update_user_permissions
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
post_lfs_json batch_url(project), body, headers
end
describe 'download' do
let(:project) { create(:project) }
let(:body) do
{
'operation' => 'download',
'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size }
]
}
shared_examples 'process authorization header' do |renew_authorization:|
let(:response_authorization) do
authorization_in_action(lfs_actions.first)
end
shared_examples 'an authorized requests' do
context 'when downloading an lfs object that is assigned to our project' do
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
if renew_authorization
context 'when the authorization comes from a user' do
it 'returns a new valid LFS token authorization' do
expect(response_authorization).not_to eq(authorization)
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
it 'returns a a valid token' do
username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
expect(username).to eq(user.username)
expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
end
it 'with href to download' do
expect(json_response).to eq({
'objects' => [
{
'oid' => sample_oid,
'size' => sample_size,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => authorization }
}
}
}
]
})
it 'generates only one new token per each request' do
authorizations = lfs_actions.map do |action|
authorization_in_action(action)
end.compact
expect(authorizations.uniq.count).to eq 1
end
end
else
context 'when the authorization comes from a token' do
it 'returns the same authorization header' do
expect(response_authorization).to eq(authorization)
end
end
end
def lfs_actions
json_response['objects'].map { |a| a['actions'] }.compact
end
context 'when downloading an lfs object that is assigned to other project' do
let(:other_project) { create(:project) }
def authorization_in_action(action)
(action['upload'] || action['download']).dig('header', 'Authorization')
end
end
describe 'download' do
let(:body) { download_body(sample_object) }
shared_examples 'an authorized request' do |renew_authorization:|
context 'when downloading an LFS object that is assigned to our project' do
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
project.lfs_objects << lfs_object
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'with href to download' do
expect(json_response).to eq({
'objects' => [
{
'oid' => sample_oid,
'size' => sample_size,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
})
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
context 'when downloading a lfs object that does not exist' do
let(:body) do
{
'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 }
]
}
context 'when downloading an LFS object that is assigned to other project' do
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'with an 404 for specific object' do
expect(json_response).to eq({
'objects' => [
{
'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
})
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
end
context 'when downloading one new and one existing lfs object' do
let(:body) do
{
'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 },
{ 'oid' => sample_oid,
'size' => sample_size }
]
}
context 'when downloading a LFS object that does not exist' do
let(:body) { download_body(non_existing_object) }
it_behaves_like 'LFS http 200 response'
it 'with an 404 for specific object' do
expect(json_response['objects'].first).to include(non_existing_object)
expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
end
context 'when downloading one new and one existing LFS object' do
let(:body) { download_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link for the new object' do
expect(json_response).to eq({
'objects' => [
{
'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it"
}
},
{
'oid' => sample_oid,
'size' => sample_size,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => authorization }
}
}
}
]
it 'responds with download hypermedia link for the new object' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
expect(json_response['objects'].last).to eq({
'oid' => non_existing_object_oid,
'size' => non_existing_object_size,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it"
}
})
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
context 'when downloading two existing LFS objects' do
let(:body) { download_body(multiple_objects) }
let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }
let(:update_lfs_permissions) do
project.lfs_objects << [lfs_object, other_object]
end
it 'responds with the download hypermedia link for each object' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
expect(json_response['objects'].last).to include(non_existing_object)
expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
end
......@@ -554,29 +504,41 @@ describe 'Git LFS API and storage' do
project.add_role(user, role)
end
it_behaves_like 'an authorized requests' do
it_behaves_like 'an authorized request', renew_authorization: true do
let(:role) { :reporter }
end
context 'when user does is not member of the project' do
let(:update_user_permissions) { nil }
it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
context 'when user does not have download access' do
let(:role) { :guest }
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
it_behaves_like 'LFS http 403 response'
end
context 'when user password is expired' do
let(:role) { :reporter}
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
it 'with an 404 for specific object' do
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
end
context 'when user is blocked' do
let(:role) { :reporter}
let(:user) { create(:user, :blocked)}
it_behaves_like 'LFS http 401 response'
end
end
context 'when using Deploy Tokens' do
let(:project) { create(:project, :repository) }
let(:authorization) { authorize_deploy_token }
let(:update_user_permissions) { nil }
let(:role) { nil }
......@@ -587,25 +549,19 @@ describe 'Git LFS API and storage' do
context 'when Deploy Token is valid' do
let(:deploy_token) { create(:deploy_token, projects: [project]) }
it_behaves_like 'an authorized requests'
it_behaves_like 'an authorized request', renew_authorization: false
end
context 'when Deploy Token is not valid' do
let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
it 'responds with access denied' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
context 'when Deploy Token is not related to the project' do
let(:another_project) { create(:project, :repository) }
let(:deploy_token) { create(:deploy_token, projects: [another_project]) }
let(:deploy_token) { create(:deploy_token, projects: [other_project]) }
it 'responds with access forbidden' do
# We render 404, to prevent data leakage about existence of the project
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
end
......@@ -616,7 +572,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
shared_examples 'can download LFS only from own projects' do
shared_examples 'can download LFS only from own projects' do |renew_authorization:|
context 'for own project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
......@@ -624,11 +580,10 @@ describe 'Git LFS API and storage' do
project.add_reporter(user)
end
it_behaves_like 'an authorized requests'
it_behaves_like 'an authorized request', renew_authorization: renew_authorization
end
context 'for other project' do
let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
......@@ -641,17 +596,16 @@ describe 'Git LFS API and storage' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end
context 'regular user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
......@@ -660,7 +614,7 @@ describe 'Git LFS API and storage' do
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'can download LFS only from own projects' do
it_behaves_like 'can download LFS only from own projects', renew_authorization: false do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
......@@ -675,11 +629,9 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
it 'responds with status 200 and href to download' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with status 200 and href to download' do
it 'returns href to download' do
expect(json_response).to eq({
'objects' => [
{
......@@ -688,7 +640,7 @@ describe 'Git LFS API and storage' do
'authenticated' => true,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'href' => objects_url(project, sample_oid),
'header' => {}
}
}
......@@ -703,37 +655,29 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
it 'responds with authorization required' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
end
end
describe 'upload' do
let(:project) { create(:project, :public) }
let(:body) do
{
'operation' => 'upload',
'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size }
]
}
end
let(:body) { upload_body(sample_object) }
shared_examples 'pushes new LFS objects' do
shared_examples 'pushes new LFS objects' do |renew_authorization:|
let(:sample_size) { 150.megabytes }
let(:sample_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
let(:sample_oid) { non_existing_object_oid }
it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first['oid']).to eq(sample_oid)
expect(json_response['objects'].first['size']).to eq(sample_size)
expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
describe 'when request is authenticated' do
......@@ -744,107 +688,80 @@ describe 'Git LFS API and storage' do
project.add_developer(user)
end
context 'when pushing an lfs object that already exists' do
let(:other_project) { create(:project) }
context 'when pushing an LFS object that already exists' do
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with links the object to the project' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first['oid']).to eq(sample_oid)
expect(json_response['objects'].first['size']).to eq(sample_size)
expect(json_response['objects'].first).to include(sample_object)
expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
end
context 'when pushing a lfs object that does not exist' do
it_behaves_like 'pushes new LFS objects'
it_behaves_like 'process authorization header', renew_authorization: true
end
context 'when pushing one new and one existing lfs object' do
let(:body) do
{
'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 },
{ 'oid' => sample_oid,
'size' => sample_size }
]
}
end
context 'when pushing a LFS object that does not exist' do
it_behaves_like 'pushes new LFS objects', renew_authorization: true
end
context 'when pushing one new and one existing LFS object' do
let(:body) { upload_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link for the new object' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
expect(json_response['objects'].first['size']).to eq(1575078)
expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first).not_to have_key('actions')
expect(json_response['objects'].last['oid']).to eq(sample_oid)
expect(json_response['objects'].last['size']).to eq(sample_size)
expect(json_response['objects'].last).not_to have_key('actions')
expect(json_response['objects'].last).to include(non_existing_object)
expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))
expect(json_response['objects'].last['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
it_behaves_like 'process authorization header', renew_authorization: true
end
end
context 'when user does not have push access' do
let(:authorization) { authorize_user }
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
context 'build has an user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
end
......@@ -856,7 +773,7 @@ describe 'Git LFS API and storage' do
project.deploy_keys_projects.create(deploy_key: key, can_push: true)
end
it_behaves_like 'pushes new LFS objects'
it_behaves_like 'pushes new LFS objects', renew_authorization: false
end
end
......@@ -866,80 +783,60 @@ describe 'Git LFS API and storage' do
project.add_maintainer(user)
end
it 'responds with status 401' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
context 'when user does not have push access' do
it 'responds with status 401' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
end
end
describe 'unsupported' do
let(:project) { create(:project) }
let(:authorization) { authorize_user }
let(:body) do
{
'operation' => 'other',
'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size }
]
}
end
let(:body) { request_body('other', sample_object) }
it 'responds with status 404' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
end
describe 'when handling lfs batch request on a read-only GitLab instance' do
describe 'when handling LFS batch request on a read-only GitLab instance' do
let(:authorization) { authorize_user }
let(:project) { create(:project) }
let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
let(:body) do
{ 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] }
end
subject { post_lfs_json(batch_url(project), body, headers) }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
project.add_maintainer(user)
enable_lfs
subject
end
it 'responds with a 200 message on download' do
post_lfs_json path, body.merge('operation' => 'download'), headers
context 'when downloading' do
let(:body) { download_body(sample_object) }
expect(response).to have_gitlab_http_status(200)
it_behaves_like 'LFS http 200 response'
end
it 'responds with a 403 message on upload' do
post_lfs_json path, body.merge('operation' => 'upload'), headers
context 'when uploading' do
let(:body) { upload_body(sample_object) }
expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 403 }
let(:message) { 'You cannot write to this read-only GitLab instance.' }
end
end
end
describe 'when pushing a lfs object' do
before do
enable_lfs
end
describe 'when pushing a LFS object' do
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
it 'responds with status 401' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
......@@ -947,9 +844,7 @@ describe 'Git LFS API and storage' do
put_finalize
end
it 'responds with status 401' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
context 'and request is sent with a malformed headers' do
......@@ -957,9 +852,7 @@ describe 'Git LFS API and storage' do
put_finalize('/etc/passwd')
end
it 'does not recognize it as a valid lfs command' do
expect(response).to have_gitlab_http_status(401)
end
it_behaves_like 'LFS http 401 response'
end
end
......@@ -969,9 +862,7 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
......@@ -979,9 +870,7 @@ describe 'Git LFS API and storage' do
put_finalize
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'and request is sent with a malformed headers' do
......@@ -989,15 +878,11 @@ describe 'Git LFS API and storage' do
put_finalize('/etc/passwd')
end
it 'does not recognize it as a valid lfs command' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
end
describe 'to one project' do
let(:project) { create(:project) }
describe 'when user is authenticated' do
let(:authorization) { authorize_user }
......@@ -1018,9 +903,7 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'uses the gitlab-workhorse content type' do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
......@@ -1029,7 +912,7 @@ describe 'Git LFS API and storage' do
shared_examples 'a local file' do
it_behaves_like 'a valid response' do
it 'responds with status 200, location of lfs store and object details' do
it 'responds with status 200, location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
expect(json_response['LfsOid']).to eq(sample_oid)
......@@ -1049,7 +932,7 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'a valid response' do
it 'responds with status 200, location of lfs remote store and object details' do
it 'responds with status 200, location of LFS remote store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
......@@ -1077,11 +960,9 @@ describe 'Git LFS API and storage' do
put_finalize
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'lfs object is linked to the project' do
it 'LFS object is linked to the project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
......@@ -1092,7 +973,7 @@ describe 'Git LFS API and storage' do
end
end
context 'and workhorse requests upload finalize for a new lfs object' do
context 'and workhorse requests upload finalize for a new LFS object' do
before do
lfs_object.destroy
end
......@@ -1202,33 +1083,25 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_ci_project }
context 'build has an user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
project.add_developer(user)
put_authorize
end
it 'responds with 403 (not 404 because the build user can read the project)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
put_authorize
end
it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like 'LFS http 404 response'
end
end
......@@ -1239,9 +1112,40 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_gitlab_http_status(404)
it_behaves_like 'LFS http 404 response'
end
end
describe 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
context 'when user allowed' do
before do
project.add_developer(user)
put_authorize
end
it_behaves_like 'LFS http 200 response'
context 'when user password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
it_behaves_like 'LFS http 401 response'
end
context 'when user is blocked' do
let(:user) { create(:user, :blocked)}
it_behaves_like 'LFS http 401 response'
end
end
context 'when user not allowed' do
before do
put_authorize
end
it_behaves_like 'LFS http 404 response'
end
end
......@@ -1268,11 +1172,9 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'with location of lfs store and object details' do
it 'with location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
......@@ -1284,11 +1186,9 @@ describe 'Git LFS API and storage' do
put_finalize
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'lfs object is linked to the source project' do
it 'LFS object is linked to the source project' do
expect(lfs_object.projects.pluck(:id)).to include(upstream_project.id)
end
end
......@@ -1307,34 +1207,24 @@ describe 'Git LFS API and storage' do
end
context 'build has an user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_gitlab_http_status(403)
end
it_behaves_like 'LFS http 403 response'
end
end
......@@ -1351,22 +1241,20 @@ describe 'Git LFS API and storage' do
upstream_project.lfs_objects << lfs_object
end
context 'when pushing the same lfs object to the second project' do
context 'when pushing the same LFS object to the second project' do
before do
finalize_headers = headers
.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
.merge(workhorse_internal_api_request_header)
put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}",
params: {},
headers: finalize_headers
put objects_url(second_project, sample_oid, sample_size),
params: {},
headers: finalize_headers
end
it 'responds with status 200' do
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'LFS http 200 response'
it 'links the lfs object to the project' do
it 'links the LFS object to the project' do
expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
end
end
......@@ -1377,7 +1265,7 @@ describe 'Git LFS API and storage' do
authorize_headers = headers
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", params: {}, headers: authorize_headers
put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
end
def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, args: {})
......@@ -1401,42 +1289,11 @@ describe 'Git LFS API and storage' do
finalize_headers = headers
finalize_headers.merge!(workhorse_internal_api_request_header) if verified
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: finalize_headers
put objects_url(project, sample_oid, sample_size), params: args, headers: finalize_headers
end
def lfs_tmp_file
"#{sample_oid}012345678"
end
end
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
def authorize_ci_project
ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end
def authorize_user
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
def authorize_deploy_key
ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
end
def authorize_user_key
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end
def authorize_deploy_token
ActionController::HttpAuthentication::Basic.encode_credentials(deploy_token.username, deploy_token.token)
end
def post_lfs_json(url, body = nil, headers = nil)
params = body.try(:to_json)
headers = (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)
post(url, params: params, headers: headers)
end
end
# frozen_string_literal: true
require_relative 'workhorse_helpers'
module LfsHttpHelpers
include WorkhorseHelpers
def authorize_ci_project
ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end
def authorize_user
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
def authorize_deploy_key
Gitlab::LfsToken.new(key).basic_encoding
end
def authorize_user_key
Gitlab::LfsToken.new(user).basic_encoding
end
def authorize_deploy_token
ActionController::HttpAuthentication::Basic.encode_credentials(deploy_token.username, deploy_token.token)
end
def post_lfs_json(url, body = nil, headers = nil)
params = body.try(:to_json)
headers = (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)
post(url, params: params, headers: headers)
end
def batch_url(project)
"#{project.http_url_to_repo}/info/lfs/objects/batch"
end
def objects_url(project, oid = nil, size = nil)
File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
end
def authorize_url(project, oid, size)
File.join(objects_url(project, oid, size), 'authorize')
end
def download_body(objects)
request_body('download', objects)
end
def upload_body(objects)
request_body('upload', objects)
end
def request_body(operation, objects)
objects = [objects] unless objects.is_a?(Array)
{
'operation' => operation,
'objects' => objects
}
end
end
# frozen_string_literal: true
shared_examples 'LFS http 200 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 200 }
end
end
shared_examples 'LFS http 401 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 401 }
end
end
shared_examples 'LFS http 403 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 403 }
let(:message) { 'Access forbidden. Check your access level.' }
end
end
shared_examples 'LFS http 501 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 }
let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' }
end
end
shared_examples 'LFS http 404 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 404 }
end
end
shared_examples 'LFS http expected response code and message' do
let(:response_code) { }
let(:message) { }
it 'responds with the expected response code and message' do
expect(response).to have_gitlab_http_status(response_code)
expect(json_response['message']).to eq(message) if message
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment