Commit b01230e7 authored by Bob Van Landuyt's avatar Bob Van Landuyt
Browse files

Rename `NamespaceValidator` to `DynamicPathValidator`

This reflects better that it validates paths instead of a namespace model
parent 1782d664ba6e
......@@ -33,7 +33,7 @@
validates :path,
presence: true,
length: { maximum: 255 },
namespace: true
dynamic_path: true
validate :nesting_level_allowed
......
......@@ -196,7 +196,7 @@
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
namespace: true,
dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message },
......
......@@ -118,7 +118,7 @@
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
dynamic_path: true,
presence: true,
uniqueness: { case_sensitive: false }
......
# DynamicPathValidator
#
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = Set.new(%w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
api
autocomplete
search
member
explore
uploads
import
notification_settings
abuse_reports
invites
help
koding
health_check
jwt
oauth
sent_notifications
]).freeze
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
WILDCARD_ROUTES = Set.new(%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges objects folders file])
STRICT_RESERVED = (TOP_LEVEL_ROUTES | WILDCARD_ROUTES).freeze
def self.valid_full_path?(full_path)
path_segments = full_path.split('/')
root_segment = path_segments.shift
valid?(root_segment, type: :top_level) && valid_wildcard_segments?(path_segments)
end
def self.valid_wildcard_segments?(segments)
segments.all? { |segment| valid?(segment, type: :wildcard) }
end
def self.valid?(value, type: :strict)
!reserved?(value, type: type) && follow_format?(value)
end
def self.reserved?(value, type: :strict)
case type
when :wildcard
WILDCARD_ROUTES.include?(value)
when :top_level
TOP_LEVEL_ROUTES.include?(value)
else
STRICT_RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
if reserved?(value, type: validation_type(record))
record.errors.add(attribute, "#{value} is a reserved name")
end
end
def validation_type(record)
case record
when Namespace
record.has_parent? ? :wildcard : :top_level
when Project
:wildcard
when User
:top_level
else
:strict
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = Set.new(%w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
api
autocomplete
search
member
explore
uploads
import
notification_settings
abuse_reports
invites
help
koding
health_check
jwt
oauth
sent_notifications
]).freeze
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
WILDCARD_ROUTES = Set.new(%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges objects folders file])
STRICT_RESERVED = (TOP_LEVEL_ROUTES | WILDCARD_ROUTES).freeze
def self.valid_full_path?(full_path)
path_segments = full_path.split('/')
root_segment = path_segments.shift
valid?(root_segment, type: :top_level) && valid_wildcard_segments?(path_segments)
end
def self.valid_wildcard_segments?(segments)
segments.all? { |segment| valid?(segment, type: :wildcard) }
end
def self.valid?(value, type: :strict)
!reserved?(value, type: type) && follow_format?(value)
end
def self.reserved?(value, type: :strict)
case type
when :wildcard
WILDCARD_ROUTES.include?(value)
when :top_level
TOP_LEVEL_ROUTES.include?(value)
else
STRICT_RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
if reserved?(value, type: validation_type(record))
record.errors.add(attribute, "#{value} is a reserved name")
end
end
def validation_type(record)
case record
when Namespace
record.has_parent? ? :wildcard : :top_level
when Project
:wildcard
else
:strict
end
end
end
......@@ -2,7 +2,7 @@
def matches?(request)
id = request.params[:id]
return false unless NamespaceValidator.valid_full_path?(id)
return false unless DynamicPathValidator.valid_full_path?(id)
Group.find_by_full_path(id).present?
end
......
......@@ -4,7 +4,7 @@
project_path = request.params[:project_id] || request.params[:id]
full_path = namespace_path + '/' + project_path
unless NamespaceValidator.valid_full_path?(full_path)
unless DynamicPathValidator.valid_full_path?(full_path)
return false
end
......
......@@ -10,7 +10,7 @@
# - Ending in `issues/id`/rendered_title` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues renderred_title
commit pipelines merge_requests new].freeze
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
......
require 'spec_helper'
describe DynamicPathValidator do
let(:validator) { described_class.new(attributes: [:path]) }
# Pass in a full path to remove the format segment:
# `/ci/lint(.:format)` -> `/ci/lint`
def without_format(path)
path.split('(', 2)[0]
end
# Pass in a full path and get the last segment before a wildcard
# That's not a parameter
# `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
# -> 'artifacts'
def segment_before_last_wildcard(path)
path_segments = path.split('/').reject { |segment| segment.empty? }
last_wildcard_index = path_segments.rindex { |part| part.starts_with?('*') }
index_of_non_param_segment = last_wildcard_index - 1
part_before_wildcard = path_segments[index_of_non_param_segment]
while parameter?(part_before_wildcard)
index_of_non_param_segment -= 1
part_before_wildcard = path_segments[index_of_non_param_segment]
end
part_before_wildcard
end
def parameter?(path_segment)
path_segment.starts_with?(':') || path_segment.starts_with?('*')
end
let(:all_routes) do
Rails.application.routes.routes.routes.
map { |r| r.path.spec.to_s }
end
let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
# Routes not starting with `/:` or `/*`
# all routes not starting with a param
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
# All routes that start with a namespaced path, that have 1 or more
# path-segments before having another wildcard parameter.
# - Starting with paths:
# - `/*namespace_id/:project_id/`
# - `/*namespace_id/:id/`
# - Followed by one or more path-parts not starting with `:` or `/`
# - Followed by a path-part that includes a wildcard parameter `*`
# At the time of writing these routes match: http://rubular.com/r/QDxulzZlxZ
STARTING_WITH_NAMESPACE = /^\/\*namespace_id\/:(project_)?id/
NON_PARAM_PARTS = /[^:*][a-z\-_\/]*/
ANY_OTHER_PATH_PART = /[a-z\-_\/:]*/
WILDCARD_SEGMENT = /\*/
let(:namespaced_wildcard_routes) do
routes_without_format.select do |p|
p =~ %r{#{STARTING_WITH_NAMESPACE}\/#{NON_PARAM_PARTS}\/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
end
end
describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do
top_level_words = routes_not_starting_in_wildcard.
map { |p| p.split('/')[1] }. # Get the first part of the path
compact.
uniq
expect(described_class::TOP_LEVEL_ROUTES).to include(*top_level_words)
end
end
describe 'WILDCARD_ROUTES' do
it 'includes all paths that can be used after a namespace/project path' do
all_wildcard_paths = namespaced_wildcard_routes.map do |path|
segment_before_last_wildcard(path)
end.uniq
expect(described_class::WILDCARD_ROUTES).to include(*all_wildcard_paths)
end
end
describe '#valid_full_path' do
it "isn't valid when the top level is reserved" do
test_path = 'u/should-be-a/reserved-word'
expect(described_class.valid_full_path?(test_path)).to be(false)
end
it "isn't valid if any of the path segments is reserved" do
test_path = 'the-wildcard/wikis/is-a-reserved-path'
expect(described_class.valid_full_path?(test_path)).to be(false)
end
it "is valid if the path doen't contain reserved words" do
test_path = 'there-are/no-wildcards/in-this-path'
expect(described_class.valid_full_path?(test_path)).to be(true)
end
end
describe '#validation_type' do
it 'uses top level validation for groups without parent' do
group = build(:group)
type = validator.validation_type(group)
expect(type).to eq(:top_level)
end
it 'uses wildcard validation for groups with a parent' do
group = build(:group, parent: create(:group))
type = validator.validation_type(group)
expect(type).to eq(:wildcard)
end
it 'uses wildcard validation for a project' do
project = build(:project)
type = validator.validation_type(project)
expect(type).to eq(:wildcard)
end
it 'uses strict validation for everything else' do
type = validator.validation_type(double)
expect(type).to eq(:strict)
end
end
end
require 'spec_helper'
describe NamespaceValidator do
let(:validator) { described_class.new(attributes: [:path]) }
# Pass in a full path to remove the format segment:
# `/ci/lint(.:format)` -> `/ci/lint`
def without_format(path)
path.split('(', 2)[0]
end
# Pass in a full path and get the last segment before a wildcard
# That's not a parameter
# `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
# -> 'artifacts'
def segment_before_last_wildcard(path)
path_segments = path.split('/').reject { |segment| segment.empty? }
last_wildcard_index = path_segments.rindex { |part| part.starts_with?('*') }
index_of_non_param_segment = last_wildcard_index - 1
part_before_wildcard = path_segments[index_of_non_param_segment]
while parameter?(part_before_wildcard)
index_of_non_param_segment -= 1
part_before_wildcard = path_segments[index_of_non_param_segment]
end
part_before_wildcard
end
def parameter?(path_segment)
path_segment.starts_with?(':') || path_segment.starts_with?('*')
end
let(:all_routes) do
Rails.application.routes.routes.routes.
map { |r| r.path.spec.to_s }
end
let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
# Routes not starting with `/:` or `/*`
# all routes not starting with a param
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
# All routes that start with a namespaced path, that have 1 or more
# path-segments before having another wildcard parameter.
# - Starting with paths:
# - `/*namespace_id/:project_id/`
# - `/*namespace_id/:id/`
# - Followed by one or more path-parts not starting with `:` or `/`
# - Followed by a path-part that includes a wildcard parameter `*`
# At the time of writing these routes match: http://rubular.com/r/QDxulzZlxZ
STARTING_WITH_NAMESPACE = /^\/\*namespace_id\/:(project_)?id/
NON_PARAM_PARTS = /[^:*][a-z\-_\/]*/
ANY_OTHER_PATH_PART = /[a-z\-_\/:]*/
WILDCARD_SEGMENT = /\*/
let(:namespaced_wildcard_routes) do
routes_without_format.select do |p|
p =~ %r{#{STARTING_WITH_NAMESPACE}\/#{NON_PARAM_PARTS}\/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
end
end
describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do
top_level_words = routes_not_starting_in_wildcard.
map { |p| p.split('/')[1] }. # Get the first part of the path
compact.
uniq
expect(described_class::TOP_LEVEL_ROUTES).to include(*top_level_words)
end
end
describe 'WILDCARD_ROUTES' do
it 'includes all paths that can be used after a namespace/project path' do
all_wildcard_paths = namespaced_wildcard_routes.map do |path|
segment_before_last_wildcard(path)
end.uniq
expect(described_class::WILDCARD_ROUTES).to include(*all_wildcard_paths)
end
end
describe '#valid_full_path' do
it "isn't valid when the top level is reserved" do
test_path = 'u/should-be-a/reserved-word'
expect(described_class.valid_full_path?(test_path)).to be(false)
end
it "isn't valid if any of the path segments is reserved" do
test_path = 'the-wildcard/wikis/is-a-reserved-path'
expect(described_class.valid_full_path?(test_path)).to be(false)
end
it "is valid if the path doen't contain reserved words" do
test_path = 'there-are/no-wildcards/in-this-path'
expect(described_class.valid_full_path?(test_path)).to be(true)
end
end
describe '#validation_type' do
it 'uses top level validation for groups without parent' do
group = build(:group)
type = validator.validation_type(group)
expect(type).to eq(:top_level)
end
it 'uses wildcard validation for groups with a parent' do
group = build(:group, parent: create(:group))
type = validator.validation_type(group)
expect(type).to eq(:wildcard)
end
it 'uses wildcard validation for a project' do
project = build(:project)
type = validator.validation_type(project)
expect(type).to eq(:wildcard)
end
it 'uses strict validation for everything else' do
type = validator.validation_type(double)
expect(type).to eq(:strict)
end
end
end
Supports Markdown
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