Commit 516e3532 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'revert-b0498c17' into 'master'

Refactor `DynamicPathValidator` and `GitLab::Regex` some more

See merge request !11693
parents 6a88f187 c27877ca
......@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController
private
def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
end
......@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base
presence: true,
dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_format_regex,
message: Gitlab::Regex.project_path_regex_message },
format: { with: Gitlab::PathRegex.project_path_format_regex,
message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
......@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base
end
def reference_pattern
name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{
((?<namespace>#{name_pattern})\/)?
(?<project>#{name_pattern})
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
end
......
......@@ -368,7 +368,7 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
(?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x
end
......
......@@ -3,16 +3,20 @@
# 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
# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
class << self
def valid_namespace_path?(path)
"#{path}/" =~ Gitlab::Regex.full_namespace_path_regex
def valid_user_path?(path)
"#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
end
def valid_group_path?(path)
"#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end
def valid_project_path?(path)
"#{path}/" =~ Gitlab::Regex.full_project_path_regex
"#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end
end
......@@ -24,14 +28,16 @@ class DynamicPathValidator < ActiveModel::EachValidator
case record
when Project
self.class.valid_project_path?(full_path)
else
self.class.valid_namespace_path?(full_path)
when Group
self.class.valid_group_path?(full_path)
else # User or non-Group Namespace
self.class.valid_user_path?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
unless value =~ Gitlab::PathRegex.namespace_format_regex
record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
return
end
......
......@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
= f.label :username
= f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
= f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
......
......@@ -15,7 +15,7 @@
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent
......
require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
concern :access_requestable do
......@@ -85,20 +84,6 @@ Rails.application.routes.draw do
root to: "root#index"
# Since group show page is wildcard routing
# we want all other routing to be checked before matching this one
constraints(GroupUrlConstrainer.new) do
scope(path: '*id',
as: :group,
constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ },
controller: :groups) do
get '/', action: :show
patch '/', action: :update
put '/', action: :update
delete '/', action: :destroy
end
end
draw :test if Rails.env.test?
get '*unmatched_route', to: 'application#route_not_found'
......
......@@ -36,7 +36,7 @@ namespace :admin do
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
scope(as: :group) do
put :members_update
......@@ -76,10 +76,10 @@ namespace :admin do
scope(path: 'projects/*namespace_id',
as: :namespace,
constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
resources(:projects,
path: '/',
constraints: { id: Gitlab::Regex.project_route_regex },
constraints: { id: Gitlab::PathRegex.project_route_regex },
only: [:show]) do
member do
......
scope(path: '*namespace_id/:project_id',
format: nil,
constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do
scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
scope(constraints: { project_id: Gitlab::PathRegex.project_git_route_regex }, module: :projects) do
# Git HTTP clients ('git clone' etc.)
scope(controller: :git_http) do
get '/info/refs', action: :info_refs
......@@ -28,7 +28,7 @@ scope(path: '*namespace_id/:project_id',
end
# Redirect /group/project/info/refs to /group/project.git/info/refs
scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do
scope(constraints: { project_id: Gitlab::PathRegex.project_route_regex }) do
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
......
require 'constraints/group_url_constrainer'
resources :groups, only: [:index, :new, :create]
scope(path: 'groups/*group_id',
module: :groups,
as: :group,
constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
......@@ -25,7 +27,7 @@ end
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
......@@ -34,3 +36,15 @@ scope(path: 'groups/*id',
get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical
end
constraints(GroupUrlConstrainer.new) do
scope(path: '*id',
as: :group,
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ },
controller: :groups) do
get '/', action: :show
patch '/', action: :update
put '/', action: :update
delete '/', action: :destroy
end
end
......@@ -13,16 +13,16 @@ constraints(ProjectUrlConstrainer.new) do
# Otherwise, Rails will overwrite the constraint with `/.+?/`,
# which breaks some of our wildcard routes like `/blob/*id`
# and `/tree/*id` that depend on the negative lookahead inside
# `Gitlab::Regex.namespace_route_regex`, which helps the router
# `Gitlab::PathRegex.full_namespace_route_regex`, which helps the router
# determine whether a certain path segment is part of `*namespace_id`,
# `:project_id`, or `*id`.
#
# See https://github.com/rails/rails/blob/v4.2.8/actionpack/lib/action_dispatch/routing/mapper.rb#L155
scope(path: '*namespace_id',
as: :namespace,
namespace_id: Gitlab::Regex.namespace_route_regex) do
namespace_id: Gitlab::PathRegex.full_namespace_route_regex) do
scope(path: ':project_id',
constraints: { project_id: Gitlab::Regex.project_route_regex },
constraints: { project_id: Gitlab::PathRegex.project_route_regex },
module: :projects,
as: :project) do
......@@ -335,7 +335,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
constraints format: /svg/ do
get :build
get :coverage
......@@ -358,7 +358,7 @@ constraints(ProjectUrlConstrainer.new) do
resources(:projects,
path: '/',
constraints: { id: Gitlab::Regex.project_route_regex },
constraints: { id: Gitlab::PathRegex.project_route_regex },
only: [:edit, :show, :update, :destroy]) do
member do
put :transfer
......
......@@ -2,7 +2,7 @@
resource :repository, only: [:create] do
member do
get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }
end
end
......@@ -24,7 +24,7 @@ scope format: false do
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex }
# Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
......@@ -34,7 +34,7 @@ scope format: false do
end
end
scope constraints: { id: Gitlab::Regex.git_reference_regex } do
scope constraints: { id: Gitlab::PathRegex.git_reference_regex } do
resources :network, only: [:show]
resources :graphs, only: [:show] do
......
......@@ -11,19 +11,7 @@ devise_scope :user do
get '/users/almost_there' => 'confirmations#almost_there'
end
constraints(UserUrlConstrainer.new) do
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.root_namespace_route_regex }
scope(path: ':username',
as: :user,
constraints: { username: Gitlab::Regex.root_namespace_route_regex },
controller: :users) do
get '/', action: :show
end
end
scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do
scope(path: 'users/:username',
as: :user,
controller: :users) do
......@@ -34,7 +22,7 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
get :contributed, as: :contributed_projects
get :snippets
get :exists
get '/', to: redirect('/%{username}')
get '/', to: redirect('/%{username}'), as: nil
end
# Compatibility with old routing
......@@ -46,3 +34,15 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
end
constraints(UserUrlConstrainer.new) do
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
scope(path: ':username',
as: :user,
constraints: { username: Gitlab::PathRegex.root_namespace_route_regex },
controller: :users) do
get '/', action: :show
end
end
......@@ -71,7 +71,7 @@ structure.
- You need to be an Owner of a group in order to be able to create
a subgroup. For more information check the [permissions table][permissions].
- For a list of words that are not allowed to be used as group names see the
[`regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
[`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
- `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
- `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
- `GROUP_ROUTES`: are names that are reserved for all groups or projects.
......@@ -163,4 +163,4 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group
[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/regex.rb
[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb
......@@ -85,7 +85,7 @@ module API
optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
......
......@@ -72,7 +72,7 @@ module API
optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
......
class GroupUrlConstrainer
def matches?(request)
id = request.params[:group_id] || request.params[:id]
full_path = request.params[:group_id] || request.params[:id]
return false unless DynamicPathValidator.valid_namespace_path?(id)
return false unless DynamicPathValidator.valid_group_path?(full_path)
Group.find_by_full_path(id, follow_redirects: request.get?).present?
Group.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
......@@ -2,7 +2,7 @@ class ProjectUrlConstrainer
def matches?(request)
namespace_path = request.params[:namespace_id]
project_path = request.params[:project_id] || request.params[:id]
full_path = namespace_path + '/' + project_path
full_path = [namespace_path, project_path].join('/')
return false unless DynamicPathValidator.valid_project_path?(full_path)
......
class UserUrlConstrainer
def matches?(request)
User.find_by_full_path(request.params[:username], follow_redirects: request.get?).present?
full_path = request.params[:username]
return false unless DynamicPathValidator.valid_user_path?(full_path)
User.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
......@@ -10,7 +10,7 @@ module Gitlab
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
RESERVED_WORDS = Gitlab::Regex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
......
module Gitlab
module PathRegex
extend self
# 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 = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
PROJECT_WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of its parent.
GROUP_ROUTES = %w[
activity
analytics
audit_events
avatar
edit
group_members
hooks
issues
labels
ldap
ldap_group_links
merge_requests
milestones
notification_setting
pipeline_quota
projects
subgroups
].freeze
ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
# The namespace regex is used in JavaScript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
# Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
# allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
# `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze
PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze
FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/)*#{NAMESPACE_FORMAT_REGEX}}.freeze
def root_namespace_route_regex
@root_namespace_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE)
single_line_regexp %r{
(?!(#{illegal_words})/)
#{NAMESPACE_FORMAT_REGEX}
}x
end
end
def full_namespace_route_regex
@full_namespace_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE)
single_line_regexp %r{
#{root_namespace_route_regex}
(?:
/
(?!#{illegal_words}/)
#{NAMESPACE_FORMAT_REGEX}
)*
}x
end
end
def project_route_regex
@project_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE)
single_line_regexp %r{
(?!(#{illegal_words})/)
#{PROJECT_PATH_FORMAT_REGEX}
}x
end
end
def project_git_route_regex
@project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
end
def root_namespace_path_regex
@root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z}
end
def full_namespace_path_regex
@full_namespace_path_regex ||= %r{\A#{full_namespace_route_regex}/\z}
end
def project_path_regex
@project_path_regex ||= %r{\A#{project_route_regex}/\z}
end
def full_project_path_regex
@full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z}
end
def full_namespace_format_regex
@namespace_format_regex ||= /A#{FULL_NAMESPACE_FORMAT_REGEX}\z/.freeze
end
def namespace_format_regex
@namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze
end
def namespace_format_message
"can contain only letters, digits, '_', '-' and '.'. " \
"Cannot start with '-' or end in '.', '.git' or '.atom'." \
end
def project_path_format_regex
@project_path_format_regex ||= /\A#{PROJECT_PATH_FORMAT_REGEX}\z/.freeze
end
def project_path_format_message
"can contain only letters, digits, '_', '-' and '.'. " \
"Cannot start with '-', end in '.git' or end in '.atom'" \
end
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
@archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
end
def git_reference_regex
# Valid git ref regex, see:
# https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
@git_reference_regex ||= single_line_regexp %r{
(?!
(?# doesn't begins with)
\/| (?# rule #6)
(?# doesn't contain)
.*(?:
[\/.]\.| (?# rule #1,3)
\/\/| (?# rule #6)
@\{| (?# rule #8)
\\ (?# rule #9)
)
)
[^\000-\040\177~^:?*\[]+ (?# rule #4-5)
(?# doesn't end with)
(?<!\.lock) (?# rule #1)
(?<![\/.]) (?# rule #6-7)
}x
end
private
def single_line_regexp(regex)
# Turns a multiline extended regexp into a single line one,
# beacuse `rake routes` breaks on multiline regexes.
Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
end
end
end
......@@ -2,203 +2,6 @@ module Gitlab
module Regex
extend self
# 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 = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
PROJECT_WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of its parent.
GROUP_ROUTES = %w[
activity
analytics
audit_events
avatar
edit
group_members
hooks
issues
labels
ldap
ldap_group_links
merge_requests
milestones
notification_setting
pipeline_quota
projects
subgroups
].freeze
ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
# The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
# Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
# allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
# Same as NAMESPACE_REGEX_STR but allows `/` in the path.
# So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
def root_namespace_route_regex
@root_namespace_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE)
single_line_regexp %r{
(?!(#{illegal_words})/)
#{NAMESPACE_REGEX_STR}
}x
end
end
def root_namespace_path_regex
@root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z}
end
def full_namespace_path_regex
@full_namespace_path_regex ||= %r{\A#{namespace_route_regex}/\z}
end
def full_project_path_regex
@full_project_path_regex ||= %r{\A#{namespace_route_regex}/#{project_route_regex}/\z}
end
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
end
def full_namespace_regex
@full_namespace_regex ||= %r{\A#{FULL_NAMESPACE_REGEX_STR}\z}
end
def namespace_route_regex
@namespace_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE)
single_line_regexp %r{
#{root_namespace_route_regex}
(?:
/
(?!#{illegal_words}/)
#{NAMESPACE_REGEX_STR}
)*
}x
end
end
def namespace_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
"Cannot start with '-' or end in '.', '.git' or '.atom'." \
end
def namespace_name_regex
@namespace_name_regex ||= /\A[\p{Alnum}\p{Pd}_\. ]*\z/.freeze
end
......@@ -216,34 +19,6 @@ module Gitlab
"It must start with letter, digit, emoji or '_'."
end
def project_path_regex
@project_path_regex ||= %r{\A#{project_route_regex}/\z}
end
def project_route_regex
@project_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE)
single_line_regexp %r{
(?!(#{illegal_words})/)
#{PROJECT_REGEX_STR}
}x
end
end
def project_git_route_regex
@project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
end
def project_path_format_regex
@project_path_format_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze
end
def project_path_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
"Cannot start with '-', end in '.git' or end in '.atom'" \
end
def file_name_regex
@file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze
end
......@@ -252,36 +27,8 @@ module Gitlab
"can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
@archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
end
def git_reference_regex
# Valid git ref regex, see:
# https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
@git_reference_regex ||= single_line_regexp %r{
(?!
(?# doesn't begins with)
\/| (?# rule #6)
(?# doesn't contain)
.*(?:
[\/.]\.| (?# rule #1,3)
\/\/| (?# rule #6)
@\{| (?# rule #8)
\\ (?# rule #9)
)
)
[^\000-\040\177~^:?*\[]+ (?# rule #4-5)
(?# doesn't end with)
(?<!\.lock) (?# rule #1)
(?<![\/.]) (?# rule #6-7)
}x
end
def container_registry_reference_regex
git_reference_regex
Gitlab::PathRegex.git_reference_regex
end
##
......@@ -315,13 +62,5 @@ module Gitlab
"can contain only lowercase letters, digits, and '-'. " \
"Must start with a letter, and cannot end with '-'"
end
private
def single_line_regexp(regex)
# Turns a multiline extended regexp into a single line one,
# beacuse `rake routes` breaks on multiline regexes.
Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
end
end
end
......@@ -133,9 +133,13 @@ describe Import::BitbucketController do
end
context "when a namespace with the Bitbucket user's username already exists" do
let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
let!(:existing_namespace) { create(:group, name: other_username) }
context "when the namespace is owned by the GitLab user" do
before do
existing_namespace.add_owner(user)
end
it "takes the existing namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
......@@ -146,11 +150,6 @@ describe Import::BitbucketController do
end
context "when the namespace is not owned by the GitLab user" do
before do
existing_namespace.owner = create(:user)
existing_namespace.save
end
it "doesn't create a project" do
expect(Gitlab::BitbucketImport::ProjectCreator).
not_to receive(:new)
......@@ -202,10 +201,14 @@ describe Import::BitbucketController do
end
context 'user has chosen an existing nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
nested_namespace.add_owner(user)
end
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params).
......@@ -248,7 +251,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
......
......@@ -108,9 +108,13 @@ describe Import::GitlabController do
end
context "when a namespace with the GitLab.com user's username already exists" do
let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
let!(:existing_namespace) { create(:group, name: other_username) }
context "when the namespace is owned by the GitLab server user" do
before do
existing_namespace.add_owner(user)
end
it "takes the existing namespace" do
expect(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, existing_namespace, user, access_params).
......@@ -121,11 +125,6 @@ describe Import::GitlabController do
end
context "when the namespace is not owned by the GitLab server user" do
before do
existing_namespace.owner = create(:user)
existing_namespace.save
end
it "doesn't create a project" do
expect(Gitlab::GitlabImport::ProjectCreator).
not_to receive(:new)
......@@ -176,8 +175,12 @@ describe Import::GitlabController do
end
context 'user has chosen an existing nested namespace for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do
nested_namespace.add_owner(user)
end
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
......@@ -221,7 +224,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
......
......@@ -18,8 +18,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
let(:subject) { described_class.new(['parent/the-Path'], migration) }
it 'includes the namespace' do
parent = create(:namespace, path: 'parent')
child = create(:namespace, path: 'the-path', parent: parent)
parent = create(:group, path: 'parent')
child = create(:group, path: 'the-path', parent: parent)
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
......@@ -30,13 +30,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
context 'for child namespaces' do
it 'only returns child namespaces with the correct path' do
_root_namespace = create(:namespace, path: 'THE-path')
_other_path = create(:namespace,
_root_namespace = create(:group, path: 'THE-path')
_other_path = create(:group,
path: 'other',
parent: create(:namespace))
namespace = create(:namespace,
parent: create(:group))
namespace = create(:group,
path: 'the-path',
parent: create(:namespace))
parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
......@@ -45,13 +45,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
it 'has no namespaces that look the same' do
_root_namespace = create(:namespace, path: 'THE-path')
_similar_path = create(:namespace,
_root_namespace = create(:group, path: 'THE-path')
_similar_path = create(:group,
path: 'not-really-the-path',
parent: create(:namespace))
namespace = create(:namespace,
parent: create(:group))
namespace = create(:group,
path: 'the-path',
parent: create(:namespace))
parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
......@@ -62,11 +62,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
context 'for top levelnamespaces' do
it 'only returns child namespaces with the correct path' do
root_namespace = create(:namespace, path: 'the-path')
_other_path = create(:namespace, path: 'other')
_child_namespace = create(:namespace,
root_namespace = create(:group, path: 'the-path')
_other_path = create(:group, path: 'other')
_child_namespace = create(:group,
path: 'the-path',
parent: create(:namespace))
parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id)
......@@ -75,11 +75,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
it 'has no namespaces that just look the same' do
root_namespace = create(:namespace, path: 'the-path')
_similar_path = create(:namespace, path: 'not-really-the-path')
_child_namespace = create(:namespace,
root_namespace = create(:group, path: 'the-path')
_similar_path = create(:group, path: 'not-really-the-path')
_child_namespace = create(:group,
path: 'the-path',
parent: create(:namespace))
parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id)
......@@ -124,10 +124,10 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
describe "#child_ids_for_parent" do
it "collects child ids for all levels" do
parent = create(:namespace)
first_child = create(:namespace, parent: parent)
second_child = create(:namespace, parent: parent)
third_child = create(:namespace, parent: second_child)
parent = create(:group)
first_child = create(:group, parent: parent)
second_child = create(:group, parent: parent)
third_child = create(:group, parent: second_child)
all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id])
......@@ -205,9 +205,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
describe '#rename_namespaces' do
let!(:top_level_namespace) { create(:namespace, path: 'the-path') }
let!(:top_level_namespace) { create(:group, path: 'the-path') }
let!(:child_namespace) do
create(:namespace, path: 'the-path', parent: create(:namespace))
create(:group, path: 'the-path', parent: create(:group))
end
it 'renames top level namespaces the namespace' do
......
# coding: utf-8
require 'spec_helper'
describe Gitlab::PathRegex, lib: true do
# 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`
# -> 'builds/artifacts'
def path_before_wildcard(path)
path = path.gsub(STARTING_WITH_NAMESPACE, "")
path_segments = path.split('/').reject(&:empty?)
wildcard_index = path_segments.index { |segment| parameter?(segment) }
segments_before_wildcard = path_segments[0..wildcard_index - 1]
segments_before_wildcard.join('/')
end
def parameter?(segment)
segment =~ /[*:]/
end
# If the path is reserved. Then no conflicting paths can# be created for any
# route using this reserved word.
#
# Both `builds/artifacts` & `build` are covered by reserving the word
# `build`
def wildcards_include?(path)
described_class::PROJECT_WILDCARD_ROUTES.include?(path) ||
described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first)
end
def failure_message(missing_words, constant_name, migration_helper)
missing_words = Array(missing_words)
<<-MSG
Found new routes that could cause conflicts with existing namespaced routes
for groups or projects.
Add <#{missing_words.join(', ')}> to `Gitlab::PathRegex::#{constant_name}
to make sure no projects or namespaces can be created with those paths.
To rename any existing records with those paths you can use the
`Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
migration helper.
Make sure to make a note of the renamed records in the release blog post.
MSG
end
let(:all_routes) do
route_set = Rails.application.routes
routes_collection = route_set.routes
routes_array = routes_collection.routes
routes_array.map { |route| route.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{^/[:*]} } }
let(:top_level_words) do
routes_not_starting_in_wildcard.map do |route|
route.split('/')[1]
end.compact.uniq
end
# 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/Rv2pDE5Dvw
STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
WILDCARD_SEGMENT = %r{\*}
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
# This will return all paths that are used in a namespaced route
# before another wildcard path:
#
# /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
# /*namespace_id/:project_id/info/lfs/objects/*oid
# /*namespace_id/:project_id/commits/*id
# /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
# -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
let(:all_wildcard_paths) do
namespaced_wildcard_routes.map do |route|
path_before_wildcard(route)
end.uniq
end
STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
let(:group_routes) do
routes_without_format.select do |path|
path =~ STARTING_WITH_GROUP
end
end
let(:paths_after_group_id) do
group_routes.map do |route|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
end.uniq
end
describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do
failure_block = lambda do
missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
end
expect(described_class::TOP_LEVEL_ROUTES)
.to include(*top_level_words), failure_block
end
end
describe 'GROUP_ROUTES' do
it "don't contain a second wildcard" do
failure_block = lambda do
missing_words = paths_after_group_id - described_class::GROUP_ROUTES
failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
end
expect(described_class::GROUP_ROUTES)
.to include(*paths_after_group_id), failure_block
end
end
describe 'PROJECT_WILDCARD_ROUTES' do
it 'includes all paths that can be used after a namespace/project path' do
aggregate_failures do
all_wildcard_paths.each do |path|
expect(wildcards_include?(path))
.to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths')
end
end
end
end
describe '.root_namespace_path_regex' do
subject { described_class.root_namespace_path_regex }
it 'rejects top level routes' do
expect(subject).not_to match('admin/')
expect(subject).not_to match('api/')
expect(subject).not_to match('.well-known/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/')
expect(subject).to match('edit/')
expect(subject).to match('wikis/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('Users/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/blob/')
expect(subject).not_to match('blob//')
end
end
describe '.full_namespace_path_regex' do
subject { described_class.full_namespace_path_regex }
context 'at the top level' do
context 'when the final level' do
it 'rejects top level routes' do
expect(subject).not_to match('admin/')
expect(subject).not_to match('api/')
expect(subject).not_to match('.well-known/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/')
expect(subject).to match('edit/')
expect(subject).to match('wikis/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
end
context 'when more levels follow' do
it 'rejects top level routes' do
expect(subject).not_to match('admin/more/')
expect(subject).not_to match('api/more/')
expect(subject).not_to match('.well-known/more/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/more/')
expect(subject).to match('edit/more/')
expect(subject).to match('wikis/more/')
expect(subject).to match('environments/folders/')
expect(subject).to match('info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('activity/more/')
expect(subject).to match('group_members/more/')
expect(subject).to match('subgroups/more/')
end
end
end
context 'at the second level' do
context 'when the final level' do
it 'accepts top level routes' do
expect(subject).to match('root/admin/')
expect(subject).to match('root/api/')
expect(subject).to match('root/.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/')
expect(subject).not_to match('root/edit/')
expect(subject).not_to match('root/wikis/')
expect(subject).not_to match('root/environments/folders/')
expect(subject).not_to match('root/info/lfs/objects/')
end
it 'rejects group routes' do
expect(subject).not_to match('root/activity/')
expect(subject).not_to match('root/group_members/')
expect(subject).not_to match('root/subgroups/')
end
end
context 'when more levels follow' do
it 'accepts top level routes' do
expect(subject).to match('root/admin/more/')
expect(subject).to match('root/api/more/')
expect(subject).to match('root/.well-known/more/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/more/')
expect(subject).not_to match('root/edit/more/')
expect(subject).not_to match('root/wikis/more/')
expect(subject).not_to match('root/environments/folders/more/')
expect(subject).not_to match('root/info/lfs/objects/more/')
end
it 'rejects group routes' do
expect(subject).not_to match('root/activity/more/')
expect(subject).not_to match('root/group_members/more/')
expect(subject).not_to match('root/subgroups/more/')
end
end
end
it 'is not case sensitive' do
expect(subject).not_to match('root/Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/root/admin/')
expect(subject).not_to match('root/admin//')
end
end
describe '.project_path_regex' do
subject { described_class.project_path_regex }
it 'accepts top level routes' do
expect(subject).to match('admin/')
expect(subject).to match('api/')
expect(subject).to match('.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('blob/')
expect(subject).not_to match('edit/')
expect(subject).not_to match('wikis/')
expect(subject).not_to match('environments/folders/')
expect(subject).not_to match('info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/admin/')
expect(subject).not_to match('admin//')
end
end
describe '.full_project_path_regex' do
subject { described_class.full_project_path_regex }
it 'accepts top level routes' do
expect(subject).to match('root/admin/')
expect(subject).to match('root/api/')
expect(subject).to match('root/.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/')
expect(subject).not_to match('root/edit/')
expect(subject).not_to match('root/wikis/')
expect(subject).not_to match('root/environments/folders/')
expect(subject).not_to match('root/info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('root/activity/')
expect(subject).to match('root/group_members/')
expect(subject).to match('root/subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('root/Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/root/admin/')
expect(subject).not_to match('root/admin//')
end
end
describe '.namespace_format_regex' do
subject { described_class.namespace_format_regex }
it { is_expected.to match('gitlab-ce') }
it { is_expected.to match('gitlab_git') }
it { is_expected.to match('_underscore.js') }
it { is_expected.to match('100px.com') }
it { is_expected.to match('gitlab.org') }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
it { is_expected.not_to match('gitlab.org.') }
it { is_expected.not_to match('gitlab.org/') }
it { is_expected.not_to match('/gitlab.org') }
it { is_expected.not_to match('gitlab git') }
end
describe '.project_path_format_regex' do
subject { described_class.project_path_format_regex }
it { is_expected.to match('gitlab-ce') }
it { is_expected.to match('gitlab_git') }
it { is_expected.to match('_underscore.js') }
it { is_expected.to match('100px.com') }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
end
end
......@@ -2,386 +2,6 @@
require 'spec_helper'
describe Gitlab::Regex, lib: true do
# 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`
# -> 'builds/artifacts'
def path_before_wildcard(path)
path = path.gsub(STARTING_WITH_NAMESPACE, "")
path_segments = path.split('/').reject(&:empty?)
wildcard_index = path_segments.index { |segment| parameter?(segment) }
segments_before_wildcard = path_segments[0..wildcard_index - 1]
segments_before_wildcard.join('/')
end
def parameter?(segment)
segment =~ /[*:]/
end
# If the path is reserved. Then no conflicting paths can# be created for any
# route using this reserved word.
#
# Both `builds/artifacts` & `build` are covered by reserving the word
# `build`
def wildcards_include?(path)
described_class::PROJECT_WILDCARD_ROUTES.include?(path) ||
described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first)
end
def failure_message(missing_words, constant_name, migration_helper)
missing_words = Array(missing_words)
<<-MSG
Found new routes that could cause conflicts with existing namespaced routes
for groups or projects.
Add <#{missing_words.join(', ')}> to `Gitlab::Regex::#{constant_name}
to make sure no projects or namespaces can be created with those paths.
To rename any existing records with those paths you can use the
`Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
migration helper.
Make sure to make a note of the renamed records in the release blog post.
MSG
end
let(:all_routes) do
route_set = Rails.application.routes
routes_collection = route_set.routes
routes_array = routes_collection.routes
routes_array.map { |route| route.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{^/[:*]} } }
let(:top_level_words) do
routes_not_starting_in_wildcard.map do |route|
route.split('/')[1]
end.compact.uniq
end
# 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/Rv2pDE5Dvw
STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
WILDCARD_SEGMENT = %r{\*}
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
# This will return all paths that are used in a namespaced route
# before another wildcard path:
#
# /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
# /*namespace_id/:project_id/info/lfs/objects/*oid
# /*namespace_id/:project_id/commits/*id
# /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
# -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
let(:all_wildcard_paths) do
namespaced_wildcard_routes.map do |route|
path_before_wildcard(route)
end.uniq
end
STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
let(:group_routes) do
routes_without_format.select do |path|
path =~ STARTING_WITH_GROUP
end
end
let(:paths_after_group_id) do
group_routes.map do |route|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
end.uniq
end
describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do
failure_block = lambda do
missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
end
expect(described_class::TOP_LEVEL_ROUTES)
.to include(*top_level_words), failure_block
end
end
describe 'GROUP_ROUTES' do
it "don't contain a second wildcard" do
failure_block = lambda do
missing_words = paths_after_group_id - described_class::GROUP_ROUTES
failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
end
expect(described_class::GROUP_ROUTES)
.to include(*paths_after_group_id), failure_block
end
end
describe 'PROJECT_WILDCARD_ROUTES' do
it 'includes all paths that can be used after a namespace/project path' do
aggregate_failures do
all_wildcard_paths.each do |path|
expect(wildcards_include?(path))
.to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths')
end
end
end
end
describe '.root_namespace_path_regex' do
subject { described_class.root_namespace_path_regex }
it 'rejects top level routes' do
expect(subject).not_to match('admin/')
expect(subject).not_to match('api/')
expect(subject).not_to match('.well-known/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/')
expect(subject).to match('edit/')
expect(subject).to match('wikis/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('Users/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/blob/')
expect(subject).not_to match('blob//')
end
end
describe '.full_namespace_path_regex' do
subject { described_class.full_namespace_path_regex }
context 'at the top level' do
context 'when the final level' do
it 'rejects top level routes' do
expect(subject).not_to match('admin/')
expect(subject).not_to match('api/')
expect(subject).not_to match('.well-known/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/')
expect(subject).to match('edit/')
expect(subject).to match('wikis/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
end
context 'when more levels follow' do
it 'rejects top level routes' do
expect(subject).not_to match('admin/more/')
expect(subject).not_to match('api/more/')
expect(subject).not_to match('.well-known/more/')
end
it 'accepts project wildcard routes' do
expect(subject).to match('blob/more/')
expect(subject).to match('edit/more/')
expect(subject).to match('wikis/more/')
expect(subject).to match('environments/folders/')
expect(subject).to match('info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('activity/more/')
expect(subject).to match('group_members/more/')
expect(subject).to match('subgroups/more/')
end
end
end
context 'at the second level' do
context 'when the final level' do
it 'accepts top level routes' do
expect(subject).to match('root/admin/')
expect(subject).to match('root/api/')
expect(subject).to match('root/.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/')
expect(subject).not_to match('root/edit/')
expect(subject).not_to match('root/wikis/')
expect(subject).not_to match('root/environments/folders/')
expect(subject).not_to match('root/info/lfs/objects/')
end
it 'rejects group routes' do
expect(subject).not_to match('root/activity/')
expect(subject).not_to match('root/group_members/')
expect(subject).not_to match('root/subgroups/')
end
end
context 'when more levels follow' do
it 'accepts top level routes' do
expect(subject).to match('root/admin/more/')
expect(subject).to match('root/api/more/')
expect(subject).to match('root/.well-known/more/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/more/')
expect(subject).not_to match('root/edit/more/')
expect(subject).not_to match('root/wikis/more/')
expect(subject).not_to match('root/environments/folders/more/')
expect(subject).not_to match('root/info/lfs/objects/more/')
end
it 'rejects group routes' do
expect(subject).not_to match('root/activity/more/')
expect(subject).not_to match('root/group_members/more/')
expect(subject).not_to match('root/subgroups/more/')
end
end
end
it 'is not case sensitive' do
expect(subject).not_to match('root/Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/root/admin/')
expect(subject).not_to match('root/admin//')
end
end
describe '.project_path_regex' do
subject { described_class.project_path_regex }
it 'accepts top level routes' do
expect(subject).to match('admin/')
expect(subject).to match('api/')
expect(subject).to match('.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('blob/')
expect(subject).not_to match('edit/')
expect(subject).not_to match('wikis/')
expect(subject).not_to match('environments/folders/')
expect(subject).not_to match('info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('activity/')
expect(subject).to match('group_members/')
expect(subject).to match('subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/admin/')
expect(subject).not_to match('admin//')
end
end
describe '.full_project_path_regex' do
subject { described_class.full_project_path_regex }
it 'accepts top level routes' do
expect(subject).to match('root/admin/')
expect(subject).to match('root/api/')
expect(subject).to match('root/.well-known/')
end
it 'rejects project wildcard routes' do
expect(subject).not_to match('root/blob/')
expect(subject).not_to match('root/edit/')
expect(subject).not_to match('root/wikis/')
expect(subject).not_to match('root/environments/folders/')
expect(subject).not_to match('root/info/lfs/objects/')
end
it 'accepts group routes' do
expect(subject).to match('root/activity/')
expect(subject).to match('root/group_members/')
expect(subject).to match('root/subgroups/')
end
it 'is not case sensitive' do
expect(subject).not_to match('root/Blob/')
end
it 'does not allow extra slashes' do
expect(subject).not_to match('/root/admin/')
expect(subject).not_to match('root/admin//')
end
end
describe '.namespace_regex' do
subject { described_class.namespace_regex }
it { is_expected.to match('gitlab-ce') }
it { is_expected.to match('gitlab_git') }
it { is_expected.to match('_underscore.js') }
it { is_expected.to match('100px.com') }
it { is_expected.to match('gitlab.org') }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
it { is_expected.not_to match('gitlab.org.') }
it { is_expected.not_to match('gitlab.org/') }
it { is_expected.not_to match('/gitlab.org') }
it { is_expected.not_to match('gitlab git') }
end
describe '.project_path_format_regex' do
subject { described_class.project_path_format_regex }
it { is_expected.to match('gitlab-ce') }
it { is_expected.to match('gitlab_git') }
it { is_expected.to match('_underscore.js') }
it { is_expected.to match('100px.com') }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
end
describe '.project_name_regex' do
subject { described_class.project_name_regex }
......@@ -412,16 +32,4 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('9foo') }
it { is_expected.not_to match('foo-') }
end
describe '.full_namespace_regex' do
subject { described_class.full_namespace_regex }
it { is_expected.to match('gitlab.org') }
it { is_expected.to match('gitlab.org/gitlab-git') }
it { is_expected.not_to match('gitlab.org.') }
it { is_expected.not_to match('gitlab.org/') }
it { is_expected.not_to match('/gitlab.org') }
it { is_expected.not_to match('gitlab.git') }
it { is_expected.not_to match('gitlab git') }
end
end
......@@ -238,8 +238,8 @@ describe Namespace, models: true do
end
context 'in sub-groups' do
let(:parent) { create(:namespace, path: 'parent') }
let(:child) { create(:namespace, parent: parent, path: 'child') }
let(:parent) { create(:group, path: 'parent') }
let(:child) { create(:group, parent: parent, path: 'child') }
let!(:project) { create(:project_empty_repo, namespace: child) }
let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') }
let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") }
......
......@@ -100,7 +100,7 @@ describe KubernetesService, models: true, caching: true do
it 'sets the namespace to the default' do
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
end
end
end
......@@ -215,7 +215,7 @@ describe KubernetesService, models: true, caching: true do
kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
end
end
end
......
......@@ -287,7 +287,7 @@ describe API::Users do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
to eq([Gitlab::Regex.namespace_regex_message])
to eq([Gitlab::PathRegex.namespace_format_message])
end
it "is not available for non admin users" do
......@@ -459,7 +459,7 @@ describe API::Users do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
to eq([Gitlab::Regex.namespace_regex_message])
to eq([Gitlab::PathRegex.namespace_format_message])
end
it 'returns 400 if provider is missing for identity update' do
......
......@@ -209,9 +209,13 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen a namespace and name for the project' do
let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
let(:test_namespace) { create(:group, name: 'test_namespace') }
let(:test_name) { 'test_name' }
before do
test_namespace.add_owner(user)
end
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider).
......@@ -230,10 +234,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen an existing nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
nested_namespace.add_owner(user)
end
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider).
......@@ -276,7 +284,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
......
......@@ -15,31 +15,31 @@ describe DynamicPathValidator do
end
context 'for group' do
it 'calls valid_namespace_path?' do
it 'calls valid_group_path?' do
group = build(:group, :nested, path: 'activity')
expect(described_class).to receive(:valid_namespace_path?).with(group.full_path).and_call_original
expect(described_class).to receive(:valid_group_path?).with(group.full_path).and_call_original
expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey
end
end
context 'for user' do
it 'calls valid_namespace_path?' do
it 'calls valid_user_path?' do
user = build(:user, username: 'activity')
expect(described_class).to receive(:valid_namespace_path?).with(user.full_path).and_call_original
expect(described_class).to receive(:valid_user_path?).with(user.full_path).and_call_original
expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy
end
end
context 'for user namespace' do
it 'calls valid_namespace_path?' do
it 'calls valid_user_path?' do
user = create(:user, username: 'activity')
namespace = user.namespace
expect(described_class).to receive(:valid_namespace_path?).with(namespace.full_path).and_call_original
expect(described_class).to receive(:valid_user_path?).with(namespace.full_path).and_call_original
expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy
end
......@@ -52,7 +52,7 @@ describe DynamicPathValidator do
validator.validate_each(group, :path, "Path with spaces, and comma's!")
expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message)
expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
end
it 'adds a message when the path is not in the correct format' 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