85be6d83be
* upstream/master: (170 commits) support ordering of project notes in notes api Redirect to an already forked project if it exists Reschedule the migration to populate fork networks Create fork networks for forks for which the source was deleted. Fix item name and namespace text overflow in Projects dropdown Minor backport from EE fix link that was linking to `html` instead of `md` Backport epic tasklist Add timeouts for Gitaly calls SSHUploadPack over Gitaly is now OptOut fix icon colors in commit list Fix star icon color/stroke Backport border inline edit Add checkboxes to automatically run AutoDevops pipeline BE for automatic pipeline when enabling Auto DevOps I am certainly weary of debugging sidekiq but I don't think that's what was meant Ensure MRs always use branch refs for comparison Fix issue comment submit button disabled on GFM paste Lock seed-fu at the correct version in Gemfile.lock Improve indexes on merge_request_diffs ...
495 lines
13 KiB
Ruby
495 lines
13 KiB
Ruby
module API
|
|
module Helpers
|
|
include Gitlab::Utils
|
|
include Helpers::Pagination
|
|
|
|
SUDO_HEADER = "HTTP_SUDO".freeze
|
|
SUDO_PARAM = :sudo
|
|
|
|
def declared_params(options = {})
|
|
options = { include_parent_namespaces: false }.merge(options)
|
|
declared(params, options).to_h.symbolize_keys
|
|
end
|
|
|
|
def check_unmodified_since!(last_modified)
|
|
if_unmodified_since = Time.parse(headers['If-Unmodified-Since']) rescue nil
|
|
|
|
if if_unmodified_since && last_modified && last_modified > if_unmodified_since
|
|
render_api_error!('412 Precondition Failed', 412)
|
|
end
|
|
end
|
|
|
|
def destroy_conditionally!(resource, last_updated: nil)
|
|
last_updated ||= resource.updated_at
|
|
|
|
check_unmodified_since!(last_updated)
|
|
|
|
status 204
|
|
if block_given?
|
|
yield resource
|
|
else
|
|
resource.destroy
|
|
end
|
|
end
|
|
|
|
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
|
# We can't rewrite this with StrongMemoize because `sudo!` would
|
|
# actually write to `@current_user`, and `sudo?` would immediately
|
|
# call `current_user` again which reads from `@current_user`.
|
|
# We should rewrite this in a way that using StrongMemoize is possible
|
|
def current_user
|
|
return @current_user if defined?(@current_user)
|
|
|
|
@current_user = initial_current_user
|
|
|
|
Gitlab::I18n.locale = @current_user&.preferred_language
|
|
|
|
sudo!
|
|
|
|
validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
|
|
|
|
@current_user
|
|
end
|
|
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
|
|
|
def sudo?
|
|
initial_current_user != current_user
|
|
end
|
|
|
|
def user_namespace
|
|
@user_namespace ||= find_namespace!(params[:id])
|
|
end
|
|
|
|
def user_group
|
|
@group ||= find_group!(params[:id])
|
|
end
|
|
|
|
def user_project
|
|
@project ||= find_project!(params[:id])
|
|
end
|
|
|
|
def wiki_page
|
|
page = user_project.wiki.find_page(params[:slug])
|
|
|
|
page || not_found!('Wiki Page')
|
|
end
|
|
|
|
def available_labels
|
|
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
|
|
end
|
|
|
|
def find_user(id)
|
|
if id =~ /^\d+$/
|
|
User.find_by(id: id)
|
|
else
|
|
User.find_by(username: id)
|
|
end
|
|
end
|
|
|
|
def find_project(id)
|
|
if id =~ /^\d+$/
|
|
Project.find_by(id: id)
|
|
else
|
|
Project.find_by_full_path(id)
|
|
end
|
|
end
|
|
|
|
def find_project!(id)
|
|
project = find_project(id)
|
|
|
|
if can?(current_user, :read_project, project)
|
|
project
|
|
else
|
|
not_found!('Project')
|
|
end
|
|
end
|
|
|
|
def find_group(id)
|
|
if id.to_s =~ /^\d+$/
|
|
Group.find_by(id: id)
|
|
else
|
|
Group.find_by_full_path(id)
|
|
end
|
|
end
|
|
|
|
def find_group!(id)
|
|
group = find_group(id)
|
|
|
|
if can?(current_user, :read_group, group)
|
|
group
|
|
else
|
|
not_found!('Group')
|
|
end
|
|
end
|
|
|
|
def find_namespace(id)
|
|
if id.to_s =~ /^\d+$/
|
|
Namespace.find_by(id: id)
|
|
else
|
|
Namespace.find_by_full_path(id)
|
|
end
|
|
end
|
|
|
|
def find_namespace!(id)
|
|
namespace = find_namespace(id)
|
|
|
|
if can?(current_user, :read_namespace, namespace)
|
|
namespace
|
|
else
|
|
not_found!('Namespace')
|
|
end
|
|
end
|
|
|
|
def find_project_label(id)
|
|
label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
|
|
label || not_found!('Label')
|
|
end
|
|
|
|
def find_project_issue(iid)
|
|
IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
|
|
end
|
|
|
|
def find_project_merge_request(iid)
|
|
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
|
|
end
|
|
|
|
def find_project_snippet(id)
|
|
finder_params = { project: user_project }
|
|
SnippetsFinder.new(current_user, finder_params).execute.find(id)
|
|
end
|
|
|
|
def find_merge_request_with_access(iid, access_level = :read_merge_request)
|
|
merge_request = user_project.merge_requests.find_by!(iid: iid)
|
|
authorize! access_level, merge_request
|
|
merge_request
|
|
end
|
|
|
|
def find_build!(id)
|
|
user_project.builds.find(id.to_i)
|
|
end
|
|
|
|
def authenticate!
|
|
unauthorized! unless current_user
|
|
end
|
|
|
|
def authenticate_non_get!
|
|
authenticate! unless %w[GET HEAD].include?(route.request_method)
|
|
end
|
|
|
|
def authenticate_by_gitlab_shell_token!
|
|
input = params['secret_token'].try(:chomp)
|
|
unless Devise.secure_compare(secret_token, input)
|
|
unauthorized!
|
|
end
|
|
end
|
|
|
|
def authenticated_with_full_private_access!
|
|
authenticate!
|
|
forbidden! unless current_user.full_private_access?
|
|
end
|
|
|
|
def authenticated_as_admin!
|
|
authenticate!
|
|
forbidden! unless current_user.admin?
|
|
end
|
|
|
|
def authorize!(action, subject = :global)
|
|
forbidden! unless can?(current_user, action, subject)
|
|
end
|
|
|
|
def authorize_push_project
|
|
authorize! :push_code, user_project
|
|
end
|
|
|
|
def authorize_admin_project
|
|
authorize! :admin_project, user_project
|
|
end
|
|
|
|
def authorize_read_builds!
|
|
authorize! :read_build, user_project
|
|
end
|
|
|
|
def authorize_update_builds!
|
|
authorize! :update_build, user_project
|
|
end
|
|
|
|
def require_gitlab_workhorse!
|
|
unless env['HTTP_GITLAB_WORKHORSE'].present?
|
|
forbidden!('Request should be executed via GitLab Workhorse')
|
|
end
|
|
end
|
|
|
|
def require_pages_enabled!
|
|
not_found! unless user_project.pages_available?
|
|
end
|
|
|
|
def require_pages_config_enabled!
|
|
not_found! unless Gitlab.config.pages.enabled
|
|
end
|
|
|
|
def can?(object, action, subject = :global)
|
|
Ability.allowed?(object, action, subject)
|
|
end
|
|
|
|
# Checks the occurrences of required attributes, each attribute must be present in the params hash
|
|
# or a Bad Request error is invoked.
|
|
#
|
|
# Parameters:
|
|
# keys (required) - A hash consisting of keys that must be present
|
|
def required_attributes!(keys)
|
|
keys.each do |key|
|
|
bad_request!(key) unless params[key].present?
|
|
end
|
|
end
|
|
|
|
def attributes_for_keys(keys, custom_params = nil)
|
|
params_hash = custom_params || params
|
|
attrs = {}
|
|
keys.each do |key|
|
|
if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
|
|
attrs[key] = params_hash[key]
|
|
end
|
|
end
|
|
ActionController::Parameters.new(attrs).permit!
|
|
end
|
|
|
|
def filter_by_iid(items, iid)
|
|
items.where(iid: iid)
|
|
end
|
|
|
|
def filter_by_search(items, text)
|
|
items.search(text)
|
|
end
|
|
|
|
# error helpers
|
|
|
|
def forbidden!(reason = nil)
|
|
message = ['403 Forbidden']
|
|
message << " - #{reason}" if reason
|
|
render_api_error!(message.join(' '), 403)
|
|
end
|
|
|
|
def bad_request!(attribute)
|
|
message = ["400 (Bad request)"]
|
|
message << "\"" + attribute.to_s + "\" not given" if attribute
|
|
render_api_error!(message.join(' '), 400)
|
|
end
|
|
|
|
def not_found!(resource = nil)
|
|
message = ["404"]
|
|
message << resource if resource
|
|
message << "Not Found"
|
|
render_api_error!(message.join(' '), 404)
|
|
end
|
|
|
|
def unauthorized!
|
|
render_api_error!('401 Unauthorized', 401)
|
|
end
|
|
|
|
def not_allowed!
|
|
render_api_error!('405 Method Not Allowed', 405)
|
|
end
|
|
|
|
def conflict!(message = nil)
|
|
render_api_error!(message || '409 Conflict', 409)
|
|
end
|
|
|
|
def file_to_large!
|
|
render_api_error!('413 Request Entity Too Large', 413)
|
|
end
|
|
|
|
def not_modified!
|
|
render_api_error!('304 Not Modified', 304)
|
|
end
|
|
|
|
def no_content!
|
|
render_api_error!('204 No Content', 204)
|
|
end
|
|
|
|
def accepted!
|
|
render_api_error!('202 Accepted', 202)
|
|
end
|
|
|
|
def render_validation_error!(model)
|
|
if model.errors.any?
|
|
render_api_error!(model.errors.messages || '400 Bad Request', 400)
|
|
end
|
|
end
|
|
|
|
def render_spam_error!
|
|
render_api_error!({ error: 'Spam detected' }, 400)
|
|
end
|
|
|
|
def render_api_error!(message, status)
|
|
error!({ 'message' => message }, status, header)
|
|
end
|
|
|
|
def handle_api_exception(exception)
|
|
if sentry_enabled? && report_exception?(exception)
|
|
define_params_for_grape_middleware
|
|
sentry_context
|
|
Raven.capture_exception(exception, extra: params)
|
|
end
|
|
|
|
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
|
|
trace = exception.backtrace
|
|
|
|
message = "\n#{exception.class} (#{exception.message}):\n"
|
|
message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
|
|
message << " " << trace.join("\n ")
|
|
|
|
API.logger.add Logger::FATAL, message
|
|
|
|
response_message =
|
|
if Rails.env.test?
|
|
message
|
|
else
|
|
'500 Internal Server Error'
|
|
end
|
|
|
|
rack_response({ 'message' => response_message }.to_json, 500)
|
|
end
|
|
|
|
# project helpers
|
|
|
|
def reorder_projects(projects)
|
|
projects.reorder(params[:order_by] => params[:sort])
|
|
end
|
|
|
|
def project_finder_params
|
|
finder_params = {}
|
|
finder_params[:owned] = true if params[:owned].present?
|
|
finder_params[:non_public] = true if params[:membership].present?
|
|
finder_params[:starred] = true if params[:starred].present?
|
|
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
|
|
finder_params[:archived] = params[:archived]
|
|
finder_params[:search] = params[:search] if params[:search]
|
|
finder_params[:user] = params.delete(:user) if params[:user]
|
|
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
|
|
finder_params
|
|
end
|
|
|
|
# file helpers
|
|
|
|
def uploaded_file(field, uploads_path)
|
|
if params[field]
|
|
bad_request!("#{field} is not a file") unless params[field][:filename]
|
|
return params[field]
|
|
end
|
|
|
|
return nil unless params["#{field}.path"] && params["#{field}.name"]
|
|
|
|
# sanitize file paths
|
|
# this requires all paths to exist
|
|
required_attributes! %W(#{field}.path)
|
|
uploads_path = File.realpath(uploads_path)
|
|
file_path = File.realpath(params["#{field}.path"])
|
|
bad_request!('Bad file path') unless file_path.start_with?(uploads_path)
|
|
|
|
UploadedFile.new(
|
|
file_path,
|
|
params["#{field}.name"],
|
|
params["#{field}.type"] || 'application/octet-stream'
|
|
)
|
|
end
|
|
|
|
def present_file!(path, filename, content_type = 'application/octet-stream')
|
|
filename ||= File.basename(path)
|
|
header['Content-Disposition'] = "attachment; filename=#{filename}"
|
|
header['Content-Transfer-Encoding'] = 'binary'
|
|
content_type content_type
|
|
|
|
# Support download acceleration
|
|
case headers['X-Sendfile-Type']
|
|
when 'X-Sendfile'
|
|
header['X-Sendfile'] = path
|
|
body
|
|
else
|
|
file path
|
|
end
|
|
end
|
|
|
|
def present_artifacts!(artifacts_file)
|
|
return not_found! unless artifacts_file.exists?
|
|
|
|
if artifacts_file.file_storage?
|
|
present_file!(artifacts_file.path, artifacts_file.filename)
|
|
else
|
|
redirect_to(artifacts_file.url)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
|
def initial_current_user
|
|
return @initial_current_user if defined?(@initial_current_user)
|
|
|
|
begin
|
|
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
|
|
rescue Gitlab::Auth::UnauthorizedError
|
|
unauthorized!
|
|
end
|
|
end
|
|
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
|
|
|
def sudo!
|
|
return unless sudo_identifier
|
|
|
|
unauthorized! unless initial_current_user
|
|
|
|
unless initial_current_user.admin?
|
|
forbidden!('Must be admin to use sudo')
|
|
end
|
|
|
|
unless access_token
|
|
forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
|
|
end
|
|
|
|
validate_access_token!(scopes: [:sudo])
|
|
|
|
sudoed_user = find_user(sudo_identifier)
|
|
not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
|
|
|
|
@current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
|
end
|
|
|
|
def sudo_identifier
|
|
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
|
|
end
|
|
|
|
def secret_token
|
|
Gitlab::Shell.secret_token
|
|
end
|
|
|
|
def send_git_blob(repository, blob)
|
|
env['api.format'] = :txt
|
|
content_type 'text/plain'
|
|
header(*Gitlab::Workhorse.send_git_blob(repository, blob))
|
|
end
|
|
|
|
def send_git_archive(repository, ref:, format:)
|
|
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
|
|
end
|
|
|
|
def send_artifacts_entry(build, entry)
|
|
header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
|
|
end
|
|
|
|
# The Grape Error Middleware only has access to `env` but not `params` nor
|
|
# `request`. We workaround this by defining methods that returns the right
|
|
# values.
|
|
def define_params_for_grape_middleware
|
|
self.define_singleton_method(:request) { Rack::Request.new(env) }
|
|
self.define_singleton_method(:params) { request.params.symbolize_keys }
|
|
end
|
|
|
|
# We could get a Grape or a standard Ruby exception. We should only report anything that
|
|
# is clearly an error.
|
|
def report_exception?(exception)
|
|
return true unless exception.respond_to?(:status)
|
|
|
|
exception.status == 500
|
|
end
|
|
end
|
|
end
|