diff options
Diffstat (limited to 'lib/api/maven_packages.rb')
-rw-r--r-- | lib/api/maven_packages.rb | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb new file mode 100644 index 00000000000..32a45c59cfa --- /dev/null +++ b/lib/api/maven_packages.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true +module API + class MavenPackages < Grape::API::Instance + MAVEN_ENDPOINT_REQUIREMENTS = { + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + + content_type :md5, 'text/plain' + content_type :sha1, 'text/plain' + content_type :binary, 'application/octet-stream' + + rescue_from ActiveRecord::RecordInvalid do |e| + render_api_error!(e.message, 400) + end + + before do + require_packages_enabled! + authenticate_non_get! + end + + helpers ::API::Helpers::PackagesHelpers + + helpers do + def extract_format(file_name) + name, _, format = file_name.rpartition('.') + + if %w(md5 sha1).include?(format) + [name, format] + else + [file_name, format] + end + end + + def verify_package_file(package_file, uploaded_file) + stored_sha1 = Digest::SHA256.hexdigest(package_file.file_sha1) + expected_sha1 = uploaded_file.sha256 + + if stored_sha1 == expected_sha1 + no_content! + else + conflict! + end + end + + def find_project_by_path(path) + project_path = path.rpartition('/').first + Project.find_by_full_path(project_path) + end + + def jar_file?(format) + format == 'jar' + end + + def present_carrierwave_file_with_head_support!(file, supports_direct_download: true) + if head_request_on_aws_file?(file, supports_direct_download) && !file.file_storage? + return redirect(signed_head_url(file)) + end + + present_carrierwave_file!(file, supports_direct_download: supports_direct_download) + end + + def signed_head_url(file) + fog_storage = ::Fog::Storage.new(file.fog_credentials) + fog_dir = fog_storage.directories.new(key: file.fog_directory) + fog_file = fog_dir.files.new(key: file.path) + expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration + + fog_file.collection.head_url(fog_file.key, expire_at) + end + + def head_request_on_aws_file?(file, supports_direct_download) + Gitlab.config.packages.object_store.enabled && + supports_direct_download && + file.class.direct_download_enabled? && + request.head? && + file.fog_credentials[:provider] == 'AWS' + end + end + + desc 'Download the maven package file at instance level' do + detail 'This feature was introduced in GitLab 11.6' + end + params do + requires :path, type: String, desc: 'Package path' + requires :file_name, type: String, desc: 'Package file name' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + file_name, format = extract_format(params[:file_name]) + + # To avoid name collision we require project path and project package be the same. + # For packages that have different name from the project we should use + # the endpoint that includes project id + project = find_project_by_path(params[:path]) + + authorize_read_package!(project) + + package = ::Packages::Maven::PackageFinder + .new(params[:path], current_user, project: project).execute! + + package_file = ::Packages::PackageFileFinder + .new(package, file_name).execute! + + case format + when 'md5' + package_file.file_md5 + when 'sha1' + package_file.file_sha1 + else + track_event('pull_package') if jar_file?(format) + present_carrierwave_file_with_head_support!(package_file.file) + end + end + + desc 'Download the maven package file at a group level' do + detail 'This feature was introduced in GitLab 11.7' + end + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + params do + requires :path, type: String, desc: 'Package path' + requires :file_name, type: String, desc: 'Package file name' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + file_name, format = extract_format(params[:file_name]) + + group = find_group(params[:id]) + + not_found!('Group') unless can?(current_user, :read_group, group) + + package = ::Packages::Maven::PackageFinder + .new(params[:path], current_user, group: group).execute! + + authorize_read_package!(package.project) + + package_file = ::Packages::PackageFileFinder + .new(package, file_name).execute! + + case format + when 'md5' + package_file.file_md5 + when 'sha1' + package_file.file_sha1 + else + track_event('pull_package') if jar_file?(format) + + present_carrierwave_file_with_head_support!(package_file.file) + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Download the maven package file' do + detail 'This feature was introduced in GitLab 11.3' + end + params do + requires :path, type: String, desc: 'Package path' + requires :file_name, type: String, desc: 'Package file name' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + authorize_read_package!(user_project) + + file_name, format = extract_format(params[:file_name]) + + package = ::Packages::Maven::PackageFinder + .new(params[:path], current_user, project: user_project).execute! + + package_file = ::Packages::PackageFileFinder + .new(package, file_name).execute! + + case format + when 'md5' + package_file.file_md5 + when 'sha1' + package_file.file_sha1 + else + track_event('pull_package') if jar_file?(format) + + present_carrierwave_file_with_head_support!(package_file.file) + end + end + + desc 'Workhorse authorize the maven package file upload' do + detail 'This feature was introduced in GitLab 11.3' + end + params do + requires :path, type: String, desc: 'Package path' + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + authorize_upload! + + status 200 + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + ::Packages::PackageFileUploader.workhorse_authorize(has_length: true) + end + + desc 'Upload the maven package file' do + detail 'This feature was introduced in GitLab 11.3' + end + params do + requires :path, type: String, desc: 'Package path' + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + authorize_upload! + + file_name, format = extract_format(params[:file_name]) + + package = ::Packages::Maven::FindOrCreatePackageService + .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute + + case format + when 'sha1' + # After uploading a file, Maven tries to upload a sha1 and md5 version of it. + # Since we store md5/sha1 in database we simply need to validate our hash + # against one uploaded by Maven. We do this for `sha1` format. + package_file = ::Packages::PackageFileFinder + .new(package, file_name).execute! + + verify_package_file(package_file, params[:file]) + when 'md5' + nil + else + track_event('push_package') if jar_file?(format) + + file_params = { + file: params[:file], + size: params['file.size'], + file_name: file_name, + file_type: params['file.type'], + file_sha1: params['file.sha1'], + file_md5: params['file.md5'] + } + + ::Packages::CreatePackageFileService.new(package, file_params).execute + end + end + end + end +end |