Commit 0f44f727 authored by Stan Hu's avatar Stan Hu

Add feature flag to apply upload API size enforcement

This adds the `enforce_max_attachment_size_upload_api` feature flag to
enable enforcement of max attachment size in the upload API.  If the
feature is not enabled, we limit to 1 GB by
default. `GITLAB_UPLOAD_API_ALLOWLIST` can be set in the environment to
allow certain project IDs to bypass enforcement.
parent a5394f8c
# frozen_string_literal: true
class UploadService
# Temporarily introduced for upload API: https://gitlab.com/gitlab-org/gitlab/-/issues/325788
attr_accessor :override_max_attachment_size
def initialize(model, file, uploader_class = FileUploader, **uploader_context)
@model, @file, @uploader_class, @uploader_context = model, file, uploader_class, uploader_context
end
......@@ -19,6 +22,6 @@ class UploadService
attr_reader :model, :file, :uploader_class, :uploader_context
def max_attachment_size
Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
override_max_attachment_size || Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
end
---
name: enforce_max_attachment_size_upload_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57250
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325787
milestone: '13.11'
type: development
group: group::source code
default_enabled: false
......@@ -13,6 +13,8 @@ module API
feature_category :projects, ['/projects/:id/custom_attributes', '/projects/:id/custom_attributes/:key']
PROJECT_ATTACHMENT_SIZE_EXEMPT = 1.gigabyte
helpers do
# EE::API::Projects would override this method
def apply_filters(projects)
......@@ -52,6 +54,19 @@ module API
accepted!
end
def exempt_from_global_attachment_size?(user_project)
list = ::Gitlab::RackAttack::UserAllowlist.new(ENV['GITLAB_UPLOAD_API_ALLOWLIST'])
list.include?(user_project.id)
end
# Temporarily introduced for upload API: https://gitlab.com/gitlab-org/gitlab/-/issues/325788
def project_attachment_size(user_project)
return PROJECT_ATTACHMENT_SIZE_EXEMPT if exempt_from_global_attachment_size?(user_project)
return user_project.max_attachment_size if Feature.enabled?(:enforce_max_attachment_size_upload_api, user_project)
PROJECT_ATTACHMENT_SIZE_EXEMPT
end
end
helpers do
......@@ -553,7 +568,7 @@ module API
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
FileUploader.workhorse_authorize(has_length: false, maximum_size: user_project.max_attachment_size)
FileUploader.workhorse_authorize(has_length: false, maximum_size: project_attachment_size(user_project))
end
desc 'Upload a file'
......@@ -561,7 +576,9 @@ module API
requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
end
post ":id/uploads", feature_category: :not_owned do
upload = UploadService.new(user_project, params[:file]).execute
service = UploadService.new(user_project, params[:file])
service.override_max_attachment_size = project_attachment_size(user_project)
upload = service.execute
present upload, with: Entities::ProjectUpload
end
......
......@@ -9,7 +9,7 @@ RSpec.describe 'Upload an attachment', :api, :js do
let_it_be(:user) { project.owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project.id}/uploads" }
let(:api_path) { "/projects/#{project_id}/uploads" }
let(:url) { capybara_url(api(api_path)) }
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
......@@ -22,7 +22,7 @@ RSpec.describe 'Upload an attachment', :api, :js do
end
shared_examples 'for an attachment' do
it 'creates package files' do
it 'creates files' do
expect { subject }
.to change { Upload.count }.by(1)
end
......@@ -30,5 +30,15 @@ RSpec.describe 'Upload an attachment', :api, :js do
it { expect(subject.code).to eq(201) }
end
it_behaves_like 'handling file uploads', 'for an attachment'
context 'with an integer project ID' do
let(:project_id) { project.id }
it_behaves_like 'handling file uploads', 'for an attachment'
end
context 'with an encoded project ID' do
let(:project_id) { "#{project.namespace.path}%2F#{project.path}" }
it_behaves_like 'handling file uploads', 'for an attachment'
end
end
......@@ -1471,7 +1471,6 @@ RSpec.describe API::Projects do
post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(project.max_attachment_size)
end
end
......@@ -1484,6 +1483,32 @@ RSpec.describe API::Projects do
end
end
context 'with exempted project' do
before do
stub_env('GITLAB_UPLOAD_API_ALLOWLIST', project.id)
end
it "returns 200" do
post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(1.gigabyte)
end
end
context 'with upload size enforcement disabled' do
before do
stub_feature_flags(enforce_max_attachment_size_upload_api: false)
end
it "returns 200" do
post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(1.gigabyte)
end
end
context 'with no Workhorse headers' do
it "returns 403" do
post api("/projects/#{project.id}/uploads/authorize", user)
......@@ -1499,6 +1524,10 @@ RSpec.describe API::Projects do
end
it "uploads the file and returns its info" do
expect_next_instance_of(UploadService) do |instance|
expect(instance).to receive(:override_max_attachment_size=).with(project.max_attachment_size).and_call_original
end
post api("/projects/#{project.id}/uploads", user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") }
expect(response).to have_gitlab_http_status(:created)
......@@ -1508,6 +1537,34 @@ RSpec.describe API::Projects do
expect(json_response['full_path']).to start_with("/#{project.namespace.path}/#{project.path}/uploads")
end
shared_examples 'capped upload attachments' do
it "limits the upload to 1 GB" do
expect_next_instance_of(UploadService) do |instance|
expect(instance).to receive(:override_max_attachment_size=).with(1.gigabyte).and_call_original
end
post api("/projects/#{project.id}/uploads", user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") }
expect(response).to have_gitlab_http_status(:created)
end
end
context 'with exempted project' do
before do
stub_env('GITLAB_UPLOAD_API_ALLOWLIST', project.id)
end
it_behaves_like 'capped upload attachments'
end
context 'with upload size enforcement disabled' do
before do
stub_feature_flags(enforce_max_attachment_size_upload_api: false)
end
it_behaves_like 'capped upload attachments'
end
end
describe "GET /projects/:id/groups" do
......
......@@ -67,6 +67,29 @@ RSpec.describe UploadService do
it { expect(@link_to_file).to eq({}) }
end
describe '#override_max_attachment_size' do
let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
let(:service) { described_class.new(@project, txt, FileUploader) }
subject { service.execute.to_h }
before do
allow(txt).to receive(:size) { 100.megabytes.to_i }
end
it 'allows the upload' do
service.override_max_attachment_size = 101.megabytes
expect(subject.keys).to eq(%i(alt url markdown))
end
it 'disallows the upload' do
service.override_max_attachment_size = 99.megabytes
expect(subject).to eq({})
end
end
end
def upload_file(project, file)
......
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