Commit 043c6649 authored by Steve Abrams's avatar Steve Abrams

Verify workhorse request for uploads

Verify PUT requests come from workhorse for
package upload and artifact endpoints using workhorse uploads.
parent e3bee019
---
title: Add workhorse request verification to package upload endpoints
merge_request:
author:
type: security
......@@ -179,10 +179,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
authorize_upload!
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
......@@ -205,8 +202,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
require_gitlab_workhorse!
authorize_upload!
file_name, format = extract_format(params[:file_name])
......
......@@ -586,6 +586,8 @@ describe API::ConanPackages do
context 'without a file from workhorse' do
let(:params) { { file: nil } }
it_behaves_like 'package workhorse uploads'
it 'rejects the request' do
subject
......@@ -710,7 +712,7 @@ describe API::ConanPackages do
subject
expect(response).to have_gitlab_http_status(:error)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when using remote storage' do
......
......@@ -10,8 +10,8 @@ describe API::MavenPackages do
let(:package_file) { package.package_files.where('file_name like ?', '%.xml').first }
let(:jar_file) { package.package_files.where('file_name like ?', '%.jar').first }
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
let(:job) { create(:ci_build, user: user) }
let(:version) { '1.0-SNAPSHOT' }
......@@ -40,14 +40,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -66,20 +66,20 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -96,7 +96,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -105,19 +105,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -127,7 +127,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'project name is different from a package name' do
......@@ -136,7 +136,7 @@ describe API::MavenPackages do
it 'rejects request' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
......@@ -163,14 +163,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -189,20 +189,20 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -219,7 +219,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -228,19 +228,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -250,7 +250,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
def download_file(file_name, params = {}, request_headers = headers)
......@@ -271,14 +271,14 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
......@@ -296,7 +296,7 @@ describe API::MavenPackages do
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
......@@ -305,19 +305,19 @@ describe API::MavenPackages do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'denies download when no private token' do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
......@@ -327,7 +327,7 @@ describe API::MavenPackages do
download_file(package_file.file_name)
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
def download_file(file_name, params = {}, request_headers = headers)
......@@ -344,13 +344,13 @@ describe API::MavenPackages do
it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'authorizes posting package with a valid token' do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
......@@ -360,7 +360,7 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request without a valid permission' do
......@@ -368,7 +368,7 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'rejects requests that did not go through gitlab-workhorse' do
......@@ -376,13 +376,13 @@ describe API::MavenPackages do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(500)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'authorizes upload with job token' do
authorize_upload(job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
end
def authorize_upload(params = {}, request_headers = headers)
......@@ -405,13 +405,13 @@ describe API::MavenPackages do
it 'rejects requests without a file from workhorse' do
upload_file_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'rejects request without a token' do
upload_file
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request if feature is not in the license' do
......@@ -419,7 +419,7 @@ describe API::MavenPackages do
upload_file_with_token
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when params from workhorse are correct' do
......@@ -435,7 +435,13 @@ describe API::MavenPackages do
it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2f.ssh%2fauthorized_keys"), params: params, headers: headers_with_token
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
end
context 'without workhorse header' do
subject { upload_file_with_token(params) }
it_behaves_like 'package workhorse uploads'
end
context 'event tracking' do
......@@ -451,14 +457,14 @@ describe API::MavenPackages do
.and change { Packages::MavenMetadatum.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(package_file.file_name).to eq(file_upload.original_filename)
end
it 'allows upload with job token' do
upload_file(params.merge(job_token: job.token))
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(:ok)
expect(package.build_info.pipeline).to eq job.pipeline
end
......@@ -468,7 +474,7 @@ describe API::MavenPackages do
it 'rejects request' do
expect { upload_file_with_token(params) }.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(400)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include('Validation failed')
end
end
......
......@@ -172,8 +172,8 @@ describe API::NugetPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget' do
let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{project.id}/packages/nuget" }
let(:headers) { {} }
......
......@@ -72,7 +72,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
project.add_maintainer(user)
end
it_behaves_like 'returning response status', :error
it_behaves_like 'returning response status', :forbidden
end
end
end
......@@ -104,6 +104,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
end
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates nuget package files'
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end
......
......@@ -115,3 +115,17 @@ RSpec.shared_examples 'background upload schedules a file migration' do
end
end
end
shared_examples 'package workhorse uploads' do
context 'without a workhorse header' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') }
it_behaves_like 'returning response status', :forbidden
it 'logs an error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).once
subject
end
end
end
......@@ -258,11 +258,21 @@ module API
end
def require_gitlab_workhorse!
verify_workhorse_api!
unless env['HTTP_GITLAB_WORKHORSE'].present?
forbidden!('Request should be executed via GitLab Workhorse')
end
end
def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers)
rescue => e
Gitlab::ErrorTracking.track_exception(e)
forbidden!
end
def require_pages_enabled!
not_found! unless user_project.pages_available?
end
......
......@@ -1545,7 +1545,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
authorize_artifacts
expect(response).to have_gitlab_http_status(500)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'authorization token is invalid' do
......@@ -1675,6 +1675,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
context 'Is missing GitLab Workhorse token headers' do
let(:jwt_token) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') }
it 'fails to post artifacts without GitLab-Workhorse' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).once
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when setting an expire date' do
let(:default_artifacts_expire_in) {}
let(:post_data) do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment