diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 16:54:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 16:54:15 +0300 |
commit | be834a25982746ffd85252ff502df42bb88cb9d5 (patch) | |
tree | b4d6a8ba0931e12fac08f05abea33a3b8ec2c8a2 /spec/requests/api/generic_packages_spec.rb | |
parent | ee925a3597f27e92f83a50937a64068109675b3d (diff) |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc32
Diffstat (limited to 'spec/requests/api/generic_packages_spec.rb')
-rw-r--r-- | spec/requests/api/generic_packages_spec.rb | 419 |
1 files changed, 386 insertions, 33 deletions
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index ed852fe75c7..2cb686167f1 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -4,79 +4,432 @@ require 'spec_helper' RSpec.describe API::GenericPackages do let_it_be(:personal_access_token) { create(:personal_access_token) } - let_it_be(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } + 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(:user) { personal_access_token.user } + let(:ci_build) { create(:ci_build, :running, user: user) } - describe 'GET /api/v4/projects/:id/packages/generic/ping' do - let(:user) { personal_access_token.user } - let(:auth_token) { personal_access_token.token } + def auth_header + return {} if user_role == :anonymous + case authenticate_with + when :personal_access_token + personal_access_token_header + when :job_token + job_token_header + when :invalid_personal_access_token + personal_access_token_header('wrong token') + when :invalid_job_token + job_token_header('wrong token') + end + end + + def personal_access_token_header(value = nil) + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => value || personal_access_token.token } + end + + def job_token_header(value = nil) + { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } + end + + shared_examples 'secure endpoint' do before do project.add_developer(user) end - context 'packages feature is disabled' do - it 'responds with 404 Not Found' do - stub_packages_setting(enabled: false) + it 'rejects malicious request' do + subject - ping(personal_access_token: auth_token) + expect(response).to have_gitlab_http_status(:bad_request) + end + end - expect(response).to have_gitlab_http_status(:not_found) + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :forbidden + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :forbidden + 'PUBLIC' | :guest | false | :personal_access_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + authorize_upload_file(workhorse_header.merge(auth_header)) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' end end context 'generic_packages feature flag is disabled' do it 'responds with 404 Not Found' do stub_feature_flags(generic_packages: false) + project.add_developer(user) - ping(personal_access_token: auth_token) + authorize_upload_file(workhorse_header.merge(personal_access_token_header)) expect(response).to have_gitlab_http_status(:not_found) end end - context 'generic_packages feature flag is enabled' do + def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize" + + put api(url), headers: request_headers + end + end + + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + include WorkhorseHelpers + + let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } + let(:params) { { file: file_upload } } + + context 'authentication' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :guest | true | :personal_access_token | :forbidden + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :forbidden + 'PUBLIC' | :guest | false | :personal_access_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :forbidden + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + headers = workhorse_header.merge(auth_header) + + upload_file(params, headers) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'when user can upload packages and has valid credentials' do before do - stub_feature_flags(generic_packages: true) + project.add_developer(user) end - context 'authenticating using personal access token' do - it 'responds with 200 OK when valid personal access token is provided' do - ping(personal_access_token: auth_token) + it 'creates package and package file when valid personal access token is used' do + headers = workhorse_header.merge(personal_access_token_header) + + expect { upload_file(params, headers) } + .to change { project.packages.generic.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:created) - expect(response).to have_gitlab_http_status(:ok) + package = project.packages.generic.last + expect(package.name).to eq('mypackage') + expect(package.version).to eq('0.0.1') + expect(package.build_info).to be_nil + + package_file = package.package_files.last + expect(package_file.file_name).to eq('myfile.tar.gz') end + end + + it 'creates package, package file, and package build info when valid job token is used' do + headers = workhorse_header.merge(job_token_header) + + expect { upload_file(params, headers) } + .to change { project.packages.generic.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) - it 'responds with 401 Unauthorized when invalid personal access token provided' do - ping(personal_access_token: 'invalid-token') + aggregate_failures do + expect(response).to have_gitlab_http_status(:created) - expect(response).to have_gitlab_http_status(:unauthorized) + package = project.packages.generic.last + expect(package.name).to eq('mypackage') + expect(package.version).to eq('0.0.1') + expect(package.build_info.pipeline).to eq(ci_build.pipeline) + + package_file = package.package_files.last + expect(package_file.file_name).to eq('myfile.tar.gz') end end - context 'authenticating using job token' do - it 'responds with 200 OK when valid job token is provided' do - job_token = create(:ci_build, :running, user: user).token + context 'event tracking' do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header)) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + + it 'rejects request without a file from workhorse' do + headers = workhorse_header.merge(personal_access_token_header) + upload_file({}, headers) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects request without an auth token' do + upload_file(params, workhorse_header) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'rejects request without workhorse rewritten fields' do + headers = workhorse_header.merge(personal_access_token_header) + upload_file(params, headers, send_rewritten_field: false) - ping(job_token: job_token) + expect(response).to have_gitlab_http_status(:bad_request) + end - expect(response).to have_gitlab_http_status(:ok) + it 'rejects request if file size is too large' do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.generic_packages_max_file_size + 1) end - it 'responds with 401 Unauthorized when invalid job token provided' do - ping(job_token: 'invalid-token') + headers = workhorse_header.merge(personal_access_token_header) + upload_file(params, headers) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects request without workhorse header' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once - expect(response).to have_gitlab_http_status(:unauthorized) + upload_file(params, personal_access_token_header) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}" + + workhorse_finalize( + api(url), + method: :put, + file_key: :file, + params: params, + headers: request_headers, + send_rewritten_field: send_rewritten_field + ) + end + end + + describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:package) { create(:generic_package, project: project) } + let_it_be(:package_file) { create(:package_file, :generic, package: package) } + + context 'authentication' do + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :success + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :success + 'PUBLIC' | :guest | false | :personal_access_token | :success + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :success + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + download_file(auth_header) + + expect(response).to have_gitlab_http_status(expected_status) end end end - def ping(personal_access_token: nil, job_token: nil) - headers = { - Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.presence, - Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_token.presence - }.compact + context 'event tracking' do + before do + project.add_developer(user) + end + + subject { download_file(personal_access_token_header) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '../.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package/../') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package%2f%2e%2e%2f') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { download_file(personal_access_token_header, param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + it 'responds with 404 Not Found for non existing package' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'no-such-package') + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'responds with 404 Not Found for non existing package file' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: 'no-such-file') + + expect(response).to have_gitlab_http_status(:not_found) + end + + def download_file(request_headers, package_name: nil, file_name: nil) + package_name ||= package.name + file_name ||= package_file.file_name + url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}" - get api('/projects/%d/packages/generic/ping' % project.id), headers: headers + get api(url), headers: request_headers end end end |