summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/projects/lfs_api_controller.rb17
-rw-r--r--app/models/ci/artifact_blob.rb2
-rw-r--r--app/models/user.rb4
-rw-r--r--changelogs/unreleased/fj-28429-generate-lfs-token-authorization.yml5
-rw-r--r--changelogs/unreleased/preview_private_artifacts.yml5
-rw-r--r--doc/administration/high_availability/README.md107
-rw-r--r--doc/user/project/pipelines/job_artifacts.md3
-rw-r--r--lib/gitlab/auth.rb17
-rw-r--r--lib/gitlab/lfs_token.rb15
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb19
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb19
-rw-r--r--spec/lib/gitlab/auth_spec.rb68
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb40
-rw-r--r--spec/models/user_spec.rb30
-rw-r--r--spec/requests/lfs_http_spec.rb827
-rw-r--r--spec/support/helpers/lfs_http_helpers.rb62
-rw-r--r--spec/support/shared_examples/lfs_http_shared_examples.rb43
19 files changed, 706 insertions, 583 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index f8e233b2733..81c871de46b 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.9.0
+1.10.0
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 224ce75c83f..ad242a078ad 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 739f7a2437e..a1983bc5462 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -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')
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index ef00ad75683..76d4b9d6206 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 66defb4c707..5711162aa1a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/changelogs/unreleased/fj-28429-generate-lfs-token-authorization.yml b/changelogs/unreleased/fj-28429-generate-lfs-token-authorization.yml
new file mode 100644
index 00000000000..2b5ddb4ab7c
--- /dev/null
+++ b/changelogs/unreleased/fj-28429-generate-lfs-token-authorization.yml
@@ -0,0 +1,5 @@
+---
+title: Generate LFS token authorization for user LFS requests
+merge_request: 17332
+author:
+type: fixed
diff --git a/changelogs/unreleased/preview_private_artifacts.yml b/changelogs/unreleased/preview_private_artifacts.yml
new file mode 100644
index 00000000000..9f5caad624c
--- /dev/null
+++ b/changelogs/unreleased/preview_private_artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Enable preview of private artifacts
+merge_request: 16675
+author: Tuomo Ala-Vannesluoma
+type: added
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index f59709d063c..d1d7af9f02e 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -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
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 41b5f210d21..85d0abdb51a 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -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
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 53c1398d6ab..ecba0ffbc46 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -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
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index 124e34562c1..e90f3f05a33 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -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
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index c0b01e573b2..e42e35bc6e0 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -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
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
index ecc07181d09..d8c6ef4755d 100644
--- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 0365d63ea9c..3fc45bfc920 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -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'))
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 701ed1f3a1b..b2fd7bdd307 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 228d1ce9964..2b171edcfce 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -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
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index dc25e4d808e..ae34f7d1f87 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,10 +1,13 @@
+# 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
diff --git a/spec/support/helpers/lfs_http_helpers.rb b/spec/support/helpers/lfs_http_helpers.rb
new file mode 100644
index 00000000000..0537b122040
--- /dev/null
+++ b/spec/support/helpers/lfs_http_helpers.rb
@@ -0,0 +1,62 @@
+# 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
diff --git a/spec/support/shared_examples/lfs_http_shared_examples.rb b/spec/support/shared_examples/lfs_http_shared_examples.rb
new file mode 100644
index 00000000000..bcd30fe9654
--- /dev/null
+++ b/spec/support/shared_examples/lfs_http_shared_examples.rb
@@ -0,0 +1,43 @@
+# 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