Unverified Commit 6683fdcf authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets
Browse files

Add nested groups support to the routing


Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parent 423d7d48f248
......@@ -49,6 +49,14 @@ def not_found
render_404
end
def route_not_found
if current_user
not_found
else
redirect_to new_user_session_path
end
end
protected
# This filter handles both private tokens and personal access tokens
......
......@@ -174,6 +174,7 @@ def update_forks_visibility_level
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
project_path: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
......
......@@ -291,8 +291,12 @@ def by_login(login)
end
end
def find_by_username(username)
iwhere(username: username).take
end
def find_by_username!(username)
find_by!('lower(username) = ?', username.downcase)
iwhere(username: username).take!
end
def find_by_personal_access_token(token_string)
......
......@@ -35,8 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
users
].freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
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 value =~ Gitlab::Regex.namespace_regex
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
......@@ -44,10 +58,4 @@ def validate_each(record, attribute, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
private
def reserved?(value)
RESERVED.include?(value)
end
end
# ProjectPathValidator
#
# Custom validator for GitLab project path values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class ProjectPathValidator < ActiveModel::EachValidator
# 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.
#
RESERVED = (NamespaceValidator::RESERVED +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
def self.valid?(value)
!reserved?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
end
delegate :reserved?, to: :class
def validate_each(record, attribute, value)
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
......@@ -7,7 +7,7 @@
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
= nav_link(path: 'wikis#pages') do
= link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
= link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project)
= nav_link(path: 'wikis#git_access') do
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
......
---
title: Add nested groups support to the routing
merge_request: 7459
author:
require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api'
require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
concern :access_requestable do
......@@ -78,10 +79,21 @@
draw :user
draw :project
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
root to: "root#index"
get '*unmatched_route', to: 'application#not_found'
# 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
get '*unmatched_route', to: 'application#route_not_found'
end
scope constraints: { id: /.+\.git/, format: nil } do
# Git HTTP clients ('git clone' etc.)
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
scope(path: '*namespace_id/:project_id', constraints: { format: nil }) do
scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do
# Git HTTP clients ('git clone' etc.)
scope(controller: :git_http) do
get '/info/refs', action: :info_refs
post '/git-upload-pack', action: :git_upload_pack
post '/git-receive-pack', action: :git_receive_pack
end
# Git LFS API (metadata)
post '/info/lfs/objects/batch', to: 'lfs_api#batch'
post '/info/lfs/objects', to: 'lfs_api#deprecated'
get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
# Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do
post :batch
post '/', action: :deprecated
get '/*oid', action: :deprecated
end
# GitLab LFS object storage
scope constraints: { oid: /[a-f0-9]{64}/ } do
get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
# GitLab LFS object storage
scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do
get '/', action: :download
scope constraints: { size: /[0-9]+/ } do
put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
scope constraints: { size: /[0-9]+/ } do
put '/*size/authorize', action: :upload_authorize
put '/*size', action: :upload_finalize
end
end
end
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
end
# Redirect /group/project/info/refs to /group/project.git/info/refs
scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
ProjectUrlConstrainer.new.matches?(request) &&
(request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/))
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
end
end
require 'constraints/group_url_constrainer'
constraints(GroupUrlConstrainer.new) do
scope(path: ':id',
as: :group,
constraints: { id: Gitlab::Regex.namespace_route_regex },
controller: :groups) do
get '/', action: :show
patch '/', action: :update
put '/', action: :update
delete '/', action: :destroy
end
end
resources :groups, only: [:index, :new, :create]
scope(path: 'groups/:id',
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do
get :edit, as: :edit_group
......@@ -24,7 +10,7 @@
get :activity, as: :activity_group
end
scope(path: 'groups/:group_id',
scope(path: 'groups/*group_id',
module: :groups,
as: :group,
constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
......@@ -42,4 +28,4 @@
end
# Must be last route in this file
get 'groups/:id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do
member do
put :transfer
delete :remove_fork
post :archive
post :unarchive
post :housekeeping
post :toggle_star
post :preview_markdown
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
put :new_issue_address
end
require 'constraints/project_url_constrainer'
resources :projects, only: [:index, :new, :create]
draw :git_http
scope module: :projects do
draw :git_http
constraints(ProjectUrlConstrainer.new) do
scope(path: '*namespace_id', as: :namespace) do
scope(path: ':project_id',
constraints: { project_id: Gitlab::Regex.project_route_regex },
module: :projects,
as: :project) do
#
# Templates
......@@ -311,5 +298,28 @@
draw :wiki
draw :repository
end
resources(:projects,
path: '/',
constraints: { id: Gitlab::Regex.project_route_regex },
only: [:edit, :show, :update, :destroy]) do
member do
put :transfer
delete :remove_fork
post :archive
post :unarchive
post :housekeeping
post :toggle_star
post :preview_markdown
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
put :new_issue_address
end
end
end
end
......@@ -29,82 +29,60 @@
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
scope do
get(
'/blob/*id/diff',
to: 'blob#diff',
constraints: { id: /.+/, format: false },
as: :blob_diff
)
get(
'/blob/*id',
to: 'blob#show',
constraints: { id: /.+/, format: false },
as: :blob
)
delete(
'/blob/*id',
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
put(
'/blob/*id',
to: 'blob#update',
constraints: { id: /.+/, format: false }
)
post(
'/blob/*id',
to: 'blob#create',
constraints: { id: /.+/, format: false }
)
scope('/blob/*id', as: :blob, controller: :blob, constraints: { id: /.+/, format: false }) do
get :diff
get '/', action: :show
delete '/', action: :destroy
post '/', action: :create
put '/', action: :update
end
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
# File/dir history
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /.+/, format: false },
as: :commits
)
end
# File/dir history
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /.+/, format: false },
as: :commits
)
......@@ -12,6 +12,9 @@
end
constraints(UserUrlConstrainer.new) do
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.namespace_route_regex }
scope(path: ':username',
as: :user,
constraints: { username: Gitlab::Regex.namespace_route_regex },
......
WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis', to: 'wikis#create'
scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
get :git_access
get :pages
get '/', to: redirect('/%{namespace_id}/%{project_id}/wikis/home')
post '/', to: 'wikis#create'
end
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
scope(path: 'wikis/*id', as: :wiki, constraints: WIKI_SLUG_ID, format: false) do
get :edit
get :history
post :preview_markdown
get '/', action: :show
put '/', action: :update
delete '/', action: :destroy
end
end
......@@ -2,9 +2,7 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
include SharedAuthentication
include SharedIssuable
include SharedProject
include SharedNote
include SharedPaths
include SharedMarkdown
step 'And I visit project "Shop" labels page' do
visit namespace_project_labels_path(project.namespace, project)
......
module SharedDiffNote
include Spinach::DSL
include RepoHelpers
include WaitForAjax
after do
wait_for_ajax if javascript_test?
end
step 'I cancel the diff comment' do
page.within(diff_file_selector) do
......
......@@ -2,6 +2,10 @@ module SharedNote
include Spinach::DSL
include WaitForAjax
after do
wait_for_ajax if javascript_test?
end
step 'I delete a comment' do
page.within('.main-notes-list') do
find('.note').hover
......
module ConstrainerHelper
def extract_resource_path(path)
id = path.dup
id.sub!(/\A#{relative_url_root}/, '') if relative_url_root
id.sub(/\A\/+/, '').sub(/\/+\z/, '').sub(/.atom\z/, '')
end
private
def relative_url_root
if defined?(Gitlab::Application.config.relative_url_root)
Gitlab::Application.config.relative_url_root
end
end
end
require_relative 'constrainer_helper'
class GroupUrlConstrainer
include ConstrainerHelper
def matches?(request)
id = extract_resource_path(request.path)
id = request.params[:id]
return false unless valid?(id)
Group.find_by(path: id).present?
end
private
if id =~ Gitlab::Regex.namespace_regex
Group.find_by(path: id).present?
else
false
def valid?(id)
id.split('/').all? do |namespace|
NamespaceValidator.valid?(namespace)
end
end
end
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
unless ProjectPathValidator.valid?(project_path)
return false
end
Project.find_with_namespace(full_path).present?
end
end