Commit d32556e9 authored by David Fernandez's avatar David Fernandez

Fix the npm package already taken validator

It will now be only executed for packages following the naming
convention (https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention).
The validator uses Project#package_already_taken? that previously looked
for duplicates across all package type. This MR updates that function so
that it takes a mandatory argument: the package type. The duplicates
search is then done scoped to that package type

Changelog: fixed
parent 076b96b6
# frozen_string_literal: true
module Packages
module Npm
# from "@scope/package-name" return "scope" or nil
def self.scope_of(package_name)
return unless package_name
return unless package_name.starts_with?('@')
return unless package_name.include?('/')
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
end
end
...@@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord ...@@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord
validate :valid_conan_package_recipe, if: :conan? validate :valid_conan_package_recipe, if: :conan?
validate :valid_composer_global_name, if: :composer? validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm? validate :npm_package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm? validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
...@@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord ...@@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord
end end
end end
def package_already_taken def npm_package_already_taken
return unless project return unless project
return unless follows_npm_naming_convention?
if project.package_already_taken?(name) if project.package_already_taken?(name, package_type: :npm)
errors.add(:base, _('Package already exists')) errors.add(:base, _('Package already exists'))
end end
end end
# https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
def follows_npm_naming_convention?
return false unless project&.root_namespace&.path
project.root_namespace.path == ::Packages::Npm.scope_of(name)
end
def unique_debian_package_name def unique_debian_package_name
return unless debian_publication&.distribution return unless debian_publication&.distribution
......
...@@ -2566,12 +2566,15 @@ class Project < ApplicationRecord ...@@ -2566,12 +2566,15 @@ class Project < ApplicationRecord
[project&.id, root_group&.id] [project&.id, root_group&.id]
end end
def package_already_taken?(package_name) def package_already_taken?(package_name, package_type:)
namespace.root_ancestor.all_projects namespace.root_ancestor.all_projects
.joins(:packages) .joins(:packages)
.where.not(id: id) .where.not(id: id)
.merge(Packages::Package.default_scoped.with_name(package_name)) .merge(
.exists? Packages::Package.default_scoped
.with_name(package_name)
.with_package_type(package_type)
).exists?
end end
def default_branch_or_main def default_branch_or_main
......
...@@ -49,28 +49,20 @@ module API ...@@ -49,28 +49,20 @@ module API
when :project when :project
params[:id] params[:id]
when :instance when :instance
namespace_path = namespace_path_from_package_name package_name = params[:package_name]
namespace_path = ::Packages::Npm.scope_of(package_name)
next unless namespace_path next unless namespace_path
namespace = Namespace.top_most namespace = Namespace.top_most
.by_path(namespace_path) .by_path(namespace_path)
next unless namespace next unless namespace
finder = ::Packages::Npm::PackageFinder.new(params[:package_name], namespace: namespace) finder = ::Packages::Npm::PackageFinder.new(package_name, namespace: namespace)
finder.last&.project_id finder.last&.project_id
end end
end end
end end
# from "@scope/package-name" return "scope" or nil
def namespace_path_from_package_name
package_name = params[:package_name]
return unless package_name.starts_with?('@')
return unless package_name.include?('/')
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Npm do
using RSpec::Parameterized::TableSyntax
describe '.scope_of' do
subject { described_class.scope_of(package_name) }
where(:package_name, :expected_result) do
nil | nil
'test' | nil
'@test' | nil
'test/package' | nil
'@/package' | nil
'@test/package' | 'test'
'@test/' | nil
end
with_them do
it { is_expected.to eq(expected_result) }
end
end
end
...@@ -418,7 +418,7 @@ RSpec.describe Packages::Package, type: :model do ...@@ -418,7 +418,7 @@ RSpec.describe Packages::Package, type: :model do
end end
end end
describe '#package_already_taken' do describe '#npm_package_already_taken' do
context 'maven package' do context 'maven package' do
let!(:package) { create(:maven_package) } let!(:package) { create(:maven_package) }
...@@ -428,6 +428,43 @@ RSpec.describe Packages::Package, type: :model do ...@@ -428,6 +428,43 @@ RSpec.describe Packages::Package, type: :model do
expect(new_package).to be_valid expect(new_package).to be_valid
end end
end end
context 'npm package' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:second_project) { create(:project, namespace: group)}
let(:package) { build(:npm_package, project: project, name: name) }
let(:second_package) { build(:npm_package, project: second_project, name: name, version: '5.0.0') }
context 'following the naming convention' do
let(:name) { "@#{group.path}/test" }
it 'will allow the first package' do
expect(package).to be_valid
end
it 'will not allow npm package with duplicate name' do
package.save!
expect(second_package).not_to be_valid
end
end
context 'not following the naming convention' do
let(:name) { '@foobar/test' }
it 'will allow the first package' do
expect(package).to be_valid
end
it 'will allow npm package with duplicate name' do
package.save!
expect(second_package).to be_valid
end
end
end
end end
context "recipe uniqueness for conan packages" do context "recipe uniqueness for conan packages" do
......
...@@ -6839,29 +6839,36 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6839,29 +6839,36 @@ RSpec.describe Project, factory_default: :keep do
end end
describe '#package_already_taken?' do describe '#package_already_taken?' do
let(:namespace) { create(:namespace) } let_it_be(:namespace) { create(:namespace) }
let(:project) { create(:project, :public, namespace: namespace) } let_it_be(:project) { create(:project, :public, namespace: namespace) }
let!(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") } let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
context 'no package exists with the same name' do context 'no package exists with the same name' do
it 'returns false' do it 'returns false' do
result = project.package_already_taken?("@#{namespace.path}/bar") result = project.package_already_taken?("@#{namespace.path}/bar", package_type: :npm)
expect(result).to be false expect(result).to be false
end end
it 'returns false if it is the project that the package belongs to' do it 'returns false if it is the project that the package belongs to' do
result = project.package_already_taken?("@#{namespace.path}/foo") result = project.package_already_taken?("@#{namespace.path}/foo", package_type: :npm)
expect(result).to be false expect(result).to be false
end end
end end
context 'a package already exists with the same name' do context 'a package already exists with the same name' do
let(:alt_project) { create(:project, :public, namespace: namespace) } let_it_be(:alt_project) { create(:project, :public, namespace: namespace) }
it 'returns true' do it 'returns true' do
result = alt_project.package_already_taken?("@#{namespace.path}/foo") result = alt_project.package_already_taken?(package.name, package_type: :npm)
expect(result).to be true expect(result).to be true
end end
context 'for a different package type' do
it 'returns false' do
result = alt_project.package_already_taken?(package.name, package_type: :nuget)
expect(result).to be false
end
end
end end
end end
......
...@@ -228,6 +228,31 @@ RSpec.describe API::NpmProjectPackages do ...@@ -228,6 +228,31 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'handling upload with different authentications' it_behaves_like 'handling upload with different authentications'
end end
context 'with an existing package' do
let_it_be(:second_project) { create(:project, namespace: namespace) }
context 'following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@#{group.path}/test") }
let(:package_name) { "@#{group.path}/test" }
it_behaves_like 'handling invalid record with 400 error'
end
context 'not following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@any_scope/test") }
let(:package_name) { "@any_scope/test" }
it "uploads the package" do
expect { upload_package_with_token(package_name, params) }
.to change { project.packages.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'package creation fails' do context 'package creation fails' 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