Commit 087673ac authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '5811-add-maven-support-to-our-artifact-repository-mvc' into 'master'

Adds support to upload and download maven packages from/to GitLab

Closes #5811

See merge request gitlab-org/gitlab-ee!6607
parents d17e480b 3b0f7851
...@@ -207,6 +207,26 @@ production: &base ...@@ -207,6 +207,26 @@ production: &base
# endpoint: 'http://127.0.0.1:9000' # default: nil # endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Packages (maven repository so far)
packages:
enabled: true
# The location where build packages are stored (default: shared/packages).
# storage_path: shared/packages
object_store:
enabled: false
remote_directory: packages # The bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages ## GitLab Pages
pages: pages:
enabled: false enabled: false
......
...@@ -252,6 +252,17 @@ Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system ...@@ -252,6 +252,17 @@ Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system
Settings.uploads['object_store'] = ObjectStoreSettings.parse(Settings.uploads['object_store']) Settings.uploads['object_store'] = ObjectStoreSettings.parse(Settings.uploads['object_store'])
Settings.uploads['object_store']['remote_directory'] ||= 'uploads' Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
#
# Packages
#
Settings['packages'] ||= Settingslogic.new({})
Settings.packages['enabled'] = true if Settings.packages['enabled'].nil?
Settings.packages['storage_path'] = Settings.absolute(Settings.packages['storage_path'] || File.join(Settings.shared['path'], "packages"))
# Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.packages['path'] = Settings.packages['storage_path']
Settings.packages['max_size'] ||= 100 # in megabytes
Settings.packages['object_store'] = ObjectStoreSettings.parse(Settings.packages['object_store'])
# #
# Mattermost # Mattermost
# #
......
# This patches ActiveRecord so indexes for binary columns created using the # This patches ActiveRecord so indexes for binary columns created
# MySQL adapter apply a length of 20. Otherwise MySQL can't create an index on # using the MySQL adapter apply a length of 20. Otherwise MySQL can't create an
# binary columns. # index on binary columns.
module MysqlSetLengthForBinaryIndex module MysqlSetLengthForBinaryIndex
def add_index(table_name, column_names, options = {}) def add_index(table_name, column_names, options = {})
......
...@@ -1923,6 +1923,43 @@ ActiveRecord::Schema.define(version: 20180807153545) do ...@@ -1923,6 +1923,43 @@ ActiveRecord::Schema.define(version: 20180807153545) do
t.string "nonce", null: false t.string "nonce", null: false
end end
create_table "packages_maven_metadata", id: :bigserial, force: :cascade do |t|
t.integer "package_id", limit: 8, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "app_group", null: false
t.string "app_name", null: false
t.string "app_version"
t.string "path", limit: 512, null: false
end
add_index "packages_maven_metadata", ["package_id", "path"], name: "index_packages_maven_metadata_on_package_id_and_path", using: :btree
create_table "packages_package_files", id: :bigserial, force: :cascade do |t|
t.integer "package_id", limit: 8, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "size", limit: 8
t.integer "file_type"
t.integer "file_store"
t.binary "file_md5"
t.binary "file_sha1"
t.string "file_name", null: false
t.text "file", null: false
end
add_index "packages_package_files", ["package_id", "file_name"], name: "index_packages_package_files_on_package_id_and_file_name", using: :btree
create_table "packages_packages", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "name", null: false
t.string "version"
end
add_index "packages_packages", ["project_id"], name: "index_packages_packages_on_project_id", using: :btree
create_table "pages_domains", force: :cascade do |t| create_table "pages_domains", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
t.text "certificate" t.text "certificate"
...@@ -3084,6 +3121,9 @@ ActiveRecord::Schema.define(version: 20180807153545) do ...@@ -3084,6 +3121,9 @@ ActiveRecord::Schema.define(version: 20180807153545) do
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "notification_settings", "users", name: "fk_0c95e91db7", on_delete: :cascade add_foreign_key "notification_settings", "users", name: "fk_0c95e91db7", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade
add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade
add_foreign_key "packages_packages", "projects", on_delete: :cascade
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
add_foreign_key "path_locks", "projects", name: "fk_5265c98f24", on_delete: :cascade add_foreign_key "path_locks", "projects", name: "fk_5265c98f24", on_delete: :cascade
add_foreign_key "path_locks", "users" add_foreign_key "path_locks", "users"
......
# GitLab Maven Packages repository
## Configure project to use GitLab Maven Repository URL
To download packages from GitLab, you need `repository` section in your `pom.xml`.
```xml
<repositories>
<repository>
<id>gitlab-maven</id>
<url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
</repository>
</repositories>
```
To upload packages to GitLab, you need a `distributionManagement` section in your `pom.xml`.
```xml
<distributionManagement>
<snapshotRepository>
<id>gitlab-maven</id>
<url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
</snapshotRepository>
</distributionManagement>
```
In both examples, replace `PROJECT_ID` with your project ID.
If you have a private GitLab installation, replace `gitlab.com` with your domain name.
## Configure repository access
If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using a [personal access tokens][pat].
You can add a corresponding section to your `settings.xml` file:
```xml
<settings>
<servers>
<server>
<id>gitlab-maven</id>
<configuration>
<httpHeaders>
<property>
<name>Private-Token</name>
<value>REPLACE_WITH_YOUR_PRIVATE_TOKEN</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>
```
[pat]: ../profile/personal_access_tokens.md
# frozen_string_literal: true
class Packages::MavenPackageFinder
attr_reader :project, :path
def initialize(project, path)
@project = project
@path = path
end
def execute
packages.last
end
def execute!
packages.last!
end
private
def packages
project.packages.joins(:maven_metadatum)
.where(packages_maven_metadata: { path: path })
end
end
# frozen_string_literal: true
class Packages::PackageFileFinder
attr_reader :package, :file_name
def initialize(package, file_name)
@package = package
@file_name = file_name
end
def execute
package_files.last
end
def execute!
package_files.last!
end
private
def package_files
package.package_files.where(file_name: file_name)
end
end
...@@ -38,6 +38,7 @@ module EE ...@@ -38,6 +38,7 @@ module EE
has_many :protected_environments has_many :protected_environments
has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy' has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy'
accepts_nested_attributes_for :software_license_policies, allow_destroy: true accepts_nested_attributes_for :software_license_policies, allow_destroy: true
has_many :packages, class_name: 'Packages::Package'
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
......
...@@ -64,6 +64,7 @@ class License < ActiveRecord::Base ...@@ -64,6 +64,7 @@ class License < ActiveRecord::Base
protected_environments protected_environments
system_header_footer system_header_footer
custom_project_templates custom_project_templates
packages
].freeze ].freeze
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
......
# frozen_string_literal: true
module Packages
def self.table_name_prefix
'packages_'
end
end
# frozen_string_literal: true
class Packages::MavenMetadatum < ActiveRecord::Base
belongs_to :package
validates :package, presence: true
validates :path,
presence: true,
format: { with: Gitlab::Regex.maven_path_regex }
validates :app_group,
presence: true,
format: { with: Gitlab::Regex.maven_app_group_regex }
validates :app_name,
presence: true,
format: { with: Gitlab::Regex.maven_app_name_regex }
end
# frozen_string_literal: true
class Packages::Package < ActiveRecord::Base
belongs_to :project
has_many :package_files
has_one :maven_metadatum, inverse_of: :package
accepts_nested_attributes_for :maven_metadatum
validates :project, presence: true
validates :name,
presence: true,
format: { with: Gitlab::Regex.package_name_regex }
end
# frozen_string_literal: true
class Packages::PackageFile < ActiveRecord::Base
belongs_to :package
validates :package, presence: true
validates :file, presence: true
validates :file_name, presence: true
mount_uploader :file, Packages::PackageFileUploader
after_save :update_file_store, if: :file_changed?
def update_file_store
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
self.update_column(:file_store, file.object_store)
end
end
...@@ -89,13 +89,17 @@ module EE ...@@ -89,13 +89,17 @@ module EE
enable :read_deploy_board enable :read_deploy_board
enable :admin_issue_link enable :admin_issue_link
enable :admin_epic_issue enable :admin_epic_issue
enable :read_package
end end
rule { can?(:developer_access) }.policy do rule { can?(:developer_access) }.policy do
enable :admin_board enable :admin_board
enable :admin_vulnerability_feedback enable :admin_vulnerability_feedback
enable :create_package
end end
rule { can?(:public_access) }.enable :read_package
rule { can?(:developer_access) & security_reports_feature_available }.enable :read_project_security_dashboard rule { can?(:developer_access) & security_reports_feature_available }.enable :read_project_security_dashboard
rule { can?(:read_project) }.enable :read_vulnerability_feedback rule { can?(:read_project) }.enable :read_vulnerability_feedback
......
# frozen_string_literal: true
module Packages
class CreateMavenPackageService < BaseService
def execute
app_group, _, app_name = params[:name].rpartition('/')
app_group.tr!('/', '.')
project.packages.create!(
name: params[:name],
version: params[:version],
maven_metadatum_attributes: {
path: params[:path],
app_group: app_group,
app_name: app_name,
app_version: params[:version]
}
)
end
end
end
# frozen_string_literal: true
module Packages
class CreatePackageFileService
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def execute
package.package_files.create!(
file: params[:file],
size: params[:size],
file_name: params[:file_name],
file_type: params[:file_type],
file_sha1: params[:file_sha1],
file_md5: params[:file_md5]
)
end
end
end
# frozen_string_literal: true
module Packages
class FindOrCreateMavenPackageService < BaseService
MAVEN_METADATA_FILE = 'maven-metadata.xml'.freeze
def execute
package = ::Packages::MavenPackageFinder
.new(project, params[:path]).execute
unless package
if params[:file_name] == MAVEN_METADATA_FILE
# Maven uploads several files during `mvn deploy` in next order:
# - my-company/my-app/1.0-SNAPSHOT/my-app.jar
# - my-company/my-app/1.0-SNAPSHOT/my-app.pom
# - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
# - my-company/my-app/maven-metadata.xml
#
# The last xml file does not have VERSION in URL because it contains
# information about all versions.
package_name, version = params[:path], nil
else
package_name, _, version = params[:path].rpartition('/')
end
package_params = {
name: package_name,
path: params[:path],
version: version
}
package = ::Packages::CreateMavenPackageService
.new(project, current_user, package_params).execute
end
package
end
end
end
# frozen_string_literal: true
class Packages::PackageFileUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
storage_options Gitlab.config.packages
def filename
model.file_name
end
def store_dir
dynamic_segment
end
private
def dynamic_segment
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
'packages', model.package.id.to_s, 'files', model.id.to_s)
end
def disk_hash
@disk_hash ||= Digest::SHA2.hexdigest(model.package.project_id.to_s)
end
end
---
title: Add ability to upload and download maven packages from/to GitLab
merge_request: 6607
author:
type: added
# frozen_string_literal: true
class CreatePackagesPackages < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
create_table :packages_packages, id: :bigserial do |t|
t.references :project,
index: true,
foreign_key: { on_delete: :cascade },
null: false
t.timestamps_with_timezone null: false
t.string :name, null: false
t.string :version
end
end
end
# frozen_string_literal: true
class CreatePackagesPackageFiles < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :packages_package_files, id: :bigserial do |t|
t.references :package, type: :bigint, null: false
t.timestamps_with_timezone null: false
t.bigint :size
t.integer :file_type
t.integer :file_store
t.binary :file_md5
t.binary :file_sha1
t.string :file_name, null: false
t.text :file, null: false
end
add_concurrent_index :packages_package_files, [:package_id, :file_name]
add_concurrent_foreign_key :packages_package_files, :packages_packages,
column: :package_id,
on_delete: :cascade
end
def down
if foreign_keys_for(:packages_package_files, :package_id).any?
remove_foreign_key :packages_package_files, column: :package_id
end
if index_exists?(:packages_package_files, [:package_id, :file_name])
remove_concurrent_index :packages_package_files, [:package_id, :file_name]
end
if table_exists?(:packages_package_files)
drop_table :packages_package_files
end
end
end
# frozen_string_literal: true
class CreatePackagesMavenMetadata < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :packages_maven_metadata, id: :bigserial do |t|
t.references :package, type: :bigint, null: false
t.timestamps_with_timezone null: false
t.string :app_group, null: false
t.string :app_name, null: false
t.string :app_version
t.string :path, limit: 512, null: false
end
add_concurrent_index :packages_maven_metadata, [:package_id, :path]
add_concurrent_foreign_key :packages_maven_metadata, :packages_packages,
column: :package_id,
on_delete: :cascade
end
def down
if foreign_keys_for(:packages_maven_metadata, :package_id).any?
remove_foreign_key :packages_maven_metadata, column: :package_id
end
if index_exists?(:packages_maven_metadata, [:package_id, :path])
remove_concurrent_index :packages_maven_metadata, [:package_id, :path]
end
if table_exists?(:packages_maven_metadata)
drop_table :packages_maven_metadata
end
end
end
# frozen_string_literal: true
module API
class MavenPackages < Grape::API
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'
before do
require_packages_enabled!
authenticate_non_get!
authorize_packages_feature!
end
helpers do
def require_packages_enabled!
not_found! unless Gitlab.config.packages.enabled
end
def authorize_packages_feature!
forbidden! unless user_project.feature_available?(:packages)
end
def authorize_download_package!
authorize!(:read_package, user_project)
end
def authorize_create_package!
authorize!(:create_package, user_project)
end
def extract_format(file_name)
name, _, format = file_name.rpartition('.')
if %w(md5 sha1).include?(format)
[name, format]
else
[file_name, nil]
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
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_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
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_download_package!
file_name, format = extract_format(params[:file_name])
package = ::Packages::MavenPackageFinder
.new(user_project, params[:path]).execute!
package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute!
case format
when 'md5'
package_file.file_md5
when 'sha1'
package_file.file_sha1
when nil
present_carrierwave_file!(package_file.file)
end
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'
end
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
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'
optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
optional 'file.size', type: Integer, desc: %q(real size of file (generated by Workhorse))
optional 'file.md5', type: String, desc: %q(md5 checksum of the file (generated by Workhorse))
optional 'file.sha1', type: String, desc: %q(sha1 checksum of the file (generated by Workhorse))
optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file (generated by Workhorse))
end
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_create_package!
require_gitlab_workhorse!
file_name, format = extract_format(params[:file_name])
uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
package = ::Packages::FindOrCreateMavenPackageService
.new(user_project, current_user, params).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, uploaded_file)
when nil
file_params = {
file: uploaded_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
...@@ -12,6 +12,22 @@ module EE ...@@ -12,6 +12,22 @@ module EE
def environment_scope_regex_message def environment_scope_regex_message
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', '*' and spaces" "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', '*' and spaces"
end end
def package_name_regex
@package_name_regex ||= %r{\A(([\w\-\.]*)/)*([\w\-\.]*)\z}.freeze
end
def maven_path_regex
package_name_regex
end
def maven_app_name_regex
@maven_app_name_regex ||= /\A[\w\-\.]+\z/.freeze
end
def maven_app_group_regex
maven_app_name_regex
end
end end
end end
end end
# frozen_string_literal: true
FactoryBot.define do
factory :package, class: Packages::Package do
project
name 'my/company/app/my-app'
version '1-0-SNAPSHOT'
factory :maven_package do
maven_metadatum
after :create do |package|
create :package_file, :xml, package: package
create :package_file, :jar, package: package
create :package_file, :pom, package: package
end
end
end
factory :package_file, class: Packages::PackageFile do
package
trait(:jar) do
file { fixture_file_upload('ee/spec/fixtures/maven/my-app-1.0-20180724.124855-1.jar') }
file_name 'my-app-1.0-20180724.124855-1.jar'
file_sha1 '4f0bfa298744d505383fbb57c554d4f5c12d88b3'
file_type 'jar'
end
trait(:pom) do
file { fixture_file_upload('ee/spec/fixtures/maven/my-app-1.0-20180724.124855-1.pom') }
file_name 'my-app-1.0-20180724.124855-1.pom'
file_sha1 '19c975abd49e5102ca6c74a619f21e0cf0351c57'
file_type 'pom'
end
trait(:xml) do
file { fixture_file_upload('ee/spec/fixtures/maven/maven-metadata.xml') }
file_name 'maven-metadata.xml'
file_sha1 '42b1bdc80de64953b6876f5a8c644f20204011b0'
file_type 'xml'
end
end
factory :maven_metadatum, class: Packages::MavenMetadatum do
package
path 'my/company/app/my-app/1.0-SNAPSHOT'
app_group 'my.company.app'
app_name 'my-app'
app_version '1.0-SNAPSHOT'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::MavenPackageFinder do
let(:project) { create(:project) }
let(:package) { create(:maven_package, project: project) }
describe '#execute!' do
it 'returns a package' do
finder = described_class.new(project, package.maven_metadatum.path)
expect(finder.execute!).to eq(package)
end
it 'raises an error' do
finder = described_class.new(project, 'com/example/my-app/1.0-SNAPSHOT')
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::PackageFileFinder do
let(:package) { create(:maven_package) }
let(:package_file) { package.package_files.first }
describe '#execute!' do
it 'returns a package file' do
finder = described_class.new(package, package_file.file_name)
expect(finder.execute!).to eq(package_file)
end
it 'raises an error' do
finder = described_class.new(package, 'unknown.jpg')
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<versioning>
<snapshot>
<timestamp>20180724.124855</timestamp>
<buildNumber>1</buildNumber>
</snapshot>
<lastUpdated>20180724124855</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>1.0-20180724.124855-1</value>
<updated>20180724124855</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>1.0-20180724.124855-1</value>
<updated>20180724124855</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>my-app</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<distributionManagement>
<snapshotRepository>
<id>local</id>
<url>file:///tmp/maven</url>
</snapshotRepository>
</distributionManagement>
<repositories>
<repository>
<id>local</id>
<url>file:///tmp/maven</url>
</repository>
</repositories>
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
</project>
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Packages::MavenMetadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
describe '#app_name' do
it { is_expected.to allow_value("my-app").for(:app_name) }
it { is_expected.not_to allow_value("my/app").for(:app_name) }
it { is_expected.not_to allow_value("my(app)").for(:app_name) }
end
describe '#app_group' do
it { is_expected.to allow_value("my.domain.com").for(:app_group) }
it { is_expected.not_to allow_value("my/domain/com").for(:app_group) }
it { is_expected.not_to allow_value("my(domain)").for(:app_group) }
end
describe '#path' do
it { is_expected.to allow_value("my/domain/com/my-app").for(:path) }
it { is_expected.to allow_value("my/domain/com/my-app/1.0-SNAPSHOT").for(:path) }
it { is_expected.not_to allow_value("my(domain)com.my-app").for(:path) }
end
end
end
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Packages::PackageFile, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
end
end
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Packages::Package, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:package_files) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
describe '#name' do
it { is_expected.to allow_value("my/domain/com/my-app").for(:name) }
it { is_expected.to allow_value("my.app-11.07.2018").for(:name) }
it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::MavenPackages do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
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(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
before do
project.add_developer(user)
stub_licensed_features(packages: true)
end
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:package) { create(:maven_package, project: project) }
let(:maven_metadatum) { package.maven_metadatum }
let(:package_file_xml) { package.package_files.find_by(file_type: 'xml') }
context 'a public project' do
it 'returns the file' do
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file_xml.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file_xml.file_sha1)
end
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'returns the file' do
download_file_with_token(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when not enough permissions' do
project.add_guest(user)
download_file_with_token(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(403)
end
it 'denies download when no private token' do
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(404)
end
end
it 'rejects request if feature is not in the license' do
stub_licensed_features(packages: false)
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(403)
end
def download_file(file_name, params = {}, request_headers = headers)
get api("/projects/#{project.id}/packages/maven/" \
"#{maven_metadatum.path}/#{file_name}"), params, request_headers
end
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
download_file(file_name, params, request_headers)
end
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do
it 'authorizes posting package with a valid token' do
authorize_upload_with_token
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
it 'rejects request without a valid token' do
headers_with_token['Private-Token'] = 'foo'
authorize_upload_with_token
expect(response).to have_gitlab_http_status(401)
end
it 'rejects request without a valid permission' do
project.add_guest(user)
authorize_upload_with_token
expect(response).to have_gitlab_http_status(403)
end
it 'rejects requests that did not go through gitlab-workhorse' do
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
authorize_upload_with_token
expect(response).to have_gitlab_http_status(500)
end
def authorize_upload(params = {}, request_headers = headers)
put api("/projects/#{project.id}/packages/maven/com/example/my-app/1.0-SNAPSHOT/maven-metadata.xml/authorize"), params, request_headers
end
def authorize_upload_with_token(params = {}, request_headers = headers_with_token)
authorize_upload(params, request_headers)
end
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:file_upload) { fixture_file_upload('ee/spec/fixtures/maven/maven-metadata.xml') }
before do
# by configuring this path we allow to pass temp file from any path
allow(Packages::PackageFileUploader).to receive(:workhorse_upload_path).and_return('/')
end
it 'rejects requests without a file from workhorse' do
upload_file_with_token
expect(response).to have_gitlab_http_status(400)
end
it 'rejects request without a token' do
upload_file
expect(response).to have_gitlab_http_status(401)
end
it 'rejects request if feature is not in the license' do
stub_licensed_features(packages: false)
upload_file_with_token
expect(response).to have_gitlab_http_status(403)
end
context 'when params from workhorse are correct' do
let(:package) { project.packages.reload.last }
let(:package_file) { package.package_files.reload.last }
let(:params) do
{
'file.path' => file_upload.path,
'file.name' => file_upload.original_filename
}
end
it 'creates package and stores package file' do
expect { upload_file_with_token(params) }.to change { project.packages.count }.by(1)
.and change { Packages::MavenMetadatum.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(200)
expect(package_file.file_name).to eq(file_upload.original_filename)
end
end
def upload_file(params = {}, request_headers = headers)
put api("/projects/#{project.id}/packages/maven/com/example/my-app/1.0-SNAPSHOT/maven-metadata.xml"), params, request_headers
end
def upload_file_with_token(params = {}, request_headers = headers_with_token)
upload_file(params, request_headers)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::CreateMavenPackageService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:app_name) { 'my-app'.freeze }
let(:version) { '1.0-SNAPSHOT'.freeze }
let(:path) { "my/company/app/#{app_name}" }
let(:path_with_version) { "#{path}/#{version}" }
describe '#execute' do
context 'with version' do
let(:params) do
{
path: path_with_version,
name: path,
version: version
}
end
it 'creates a new package with metadatum' do
package = described_class.new(project, user, params).execute
expect(package).to be_valid
expect(package.name).to eq(path)
expect(package.version).to eq(version)
expect(package.maven_metadatum).to be_valid
expect(package.maven_metadatum.path).to eq(path_with_version)
expect(package.maven_metadatum.app_group).to eq('my.company.app')
expect(package.maven_metadatum.app_name).to eq(app_name)
expect(package.maven_metadatum.app_version).to eq(version)
end
end
context 'without version' do
let(:params) do
{
path: path,
name: path,
version: nil
}
end
it 'creates a new package with metadatum' do
package = described_class.new(project, user, params).execute
expect(package).to be_valid
expect(package.name).to eq(path)
expect(package.version).to be nil
expect(package.maven_metadatum).to be_valid
expect(package.maven_metadatum.path).to eq(path)
expect(package.maven_metadatum.app_group).to eq('my.company.app')
expect(package.maven_metadatum.app_name).to eq(app_name)
expect(package.maven_metadatum.app_version).to be nil
end
end
context 'path is missing' do
let(:params) do
{
name: path,
version: version
}
end
it 'raises an error' do
service = described_class.new(project, user, params)
expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::CreatePackageFileService do
let(:package) { create(:maven_package) }
describe '#execute' do
context 'with valid params' do
let(:params) do
{
file: Tempfile.new,
file_name: 'foo.jar'
}
end
it 'creates a new package file' do
package_file = described_class.new(package, params).execute
expect(package_file).to be_valid
expect(package_file.file_name).to eq('foo.jar')
end
end
context 'file is missing' do
let(:params) do
{
file_name: 'foo.jar'
}
end
it 'raises an error' do
service = described_class.new(package, params)
expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
end
...@@ -172,6 +172,7 @@ module API ...@@ -172,6 +172,7 @@ module API
mount ::API::License mount ::API::License
mount ::API::ProjectMirror mount ::API::ProjectMirror
mount ::API::ProjectPushRule mount ::API::ProjectPushRule
mount ::API::MavenPackages
## EE-specific API V4 endpoints END ## EE-specific API V4 endpoints END
route :any, '*path' do route :any, '*path' do
......
...@@ -348,6 +348,7 @@ project: ...@@ -348,6 +348,7 @@ project:
- software_license_policies - software_license_policies
- repository_languages - repository_languages
- project_registry - project_registry
- packages
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
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