diff options
-rw-r--r-- | GITLAB_PAGES_VERSION | 2 | ||||
-rw-r--r-- | app/controllers/application_controller.rb | 4 | ||||
-rw-r--r-- | app/controllers/projects/lfs_api_controller.rb | 17 | ||||
-rw-r--r-- | app/models/ci/artifact_blob.rb | 2 | ||||
-rw-r--r-- | app/models/user.rb | 4 | ||||
-rw-r--r-- | changelogs/unreleased/fj-28429-generate-lfs-token-authorization.yml | 5 | ||||
-rw-r--r-- | changelogs/unreleased/preview_private_artifacts.yml | 5 | ||||
-rw-r--r-- | doc/administration/high_availability/README.md | 107 | ||||
-rw-r--r-- | doc/user/project/pipelines/job_artifacts.md | 3 | ||||
-rw-r--r-- | lib/gitlab/auth.rb | 17 | ||||
-rw-r--r-- | lib/gitlab/lfs_token.rb | 15 | ||||
-rw-r--r-- | spec/controllers/projects/artifacts_controller_spec.rb | 19 | ||||
-rw-r--r-- | spec/features/projects/artifacts/user_browses_artifacts_spec.rb | 19 | ||||
-rw-r--r-- | spec/lib/gitlab/auth_spec.rb | 68 | ||||
-rw-r--r-- | spec/lib/gitlab/lfs_token_spec.rb | 40 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 30 | ||||
-rw-r--r-- | spec/requests/lfs_http_spec.rb | 827 | ||||
-rw-r--r-- | spec/support/helpers/lfs_http_helpers.rb | 62 | ||||
-rw-r--r-- | spec/support/shared_examples/lfs_http_shared_examples.rb | 43 |
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 |