PageRenderTime 24ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/api/helpers.rb

https://gitlab.com/alexkeramidas/gitlab-ce
Ruby | 499 lines | 369 code | 107 blank | 23 comment | 45 complexity | af568c08300556a93f0d77382010d269 MD5 | raw file
  1. module API
  2. module Helpers
  3. include Gitlab::Utils
  4. include Helpers::Pagination
  5. SUDO_HEADER = "HTTP_SUDO".freeze
  6. SUDO_PARAM = :sudo
  7. API_USER_ENV = 'gitlab.api.user'.freeze
  8. def declared_params(options = {})
  9. options = { include_parent_namespaces: false }.merge(options)
  10. declared(params, options).to_h.symbolize_keys
  11. end
  12. def check_unmodified_since!(last_modified)
  13. if_unmodified_since = Time.parse(headers['If-Unmodified-Since']) rescue nil
  14. if if_unmodified_since && last_modified && last_modified > if_unmodified_since
  15. render_api_error!('412 Precondition Failed', 412)
  16. end
  17. end
  18. def destroy_conditionally!(resource, last_updated: nil)
  19. last_updated ||= resource.updated_at
  20. check_unmodified_since!(last_updated)
  21. status 204
  22. if block_given?
  23. yield resource
  24. else
  25. resource.destroy
  26. end
  27. end
  28. # rubocop:disable Gitlab/ModuleWithInstanceVariables
  29. # We can't rewrite this with StrongMemoize because `sudo!` would
  30. # actually write to `@current_user`, and `sudo?` would immediately
  31. # call `current_user` again which reads from `@current_user`.
  32. # We should rewrite this in a way that using StrongMemoize is possible
  33. def current_user
  34. return @current_user if defined?(@current_user)
  35. @current_user = initial_current_user
  36. Gitlab::I18n.locale = @current_user&.preferred_language
  37. sudo!
  38. validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
  39. save_current_user_in_env(@current_user) if @current_user
  40. @current_user
  41. end
  42. # rubocop:enable Gitlab/ModuleWithInstanceVariables
  43. def save_current_user_in_env(user)
  44. env[API_USER_ENV] = { user_id: user.id, username: user.username }
  45. end
  46. def sudo?
  47. initial_current_user != current_user
  48. end
  49. def user_namespace
  50. @user_namespace ||= find_namespace!(params[:id])
  51. end
  52. def user_group
  53. @group ||= find_group!(params[:id])
  54. end
  55. def user_project
  56. @project ||= find_project!(params[:id])
  57. end
  58. def wiki_page
  59. page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
  60. page || not_found!('Wiki Page')
  61. end
  62. def available_labels_for(label_parent)
  63. search_params = { include_ancestor_groups: true }
  64. if label_parent.is_a?(Project)
  65. search_params[:project_id] = label_parent.id
  66. else
  67. search_params.merge!(group_id: label_parent.id, only_group_labels: true)
  68. end
  69. LabelsFinder.new(current_user, search_params).execute
  70. end
  71. def find_user(id)
  72. if id =~ /^\d+$/
  73. User.find_by(id: id)
  74. else
  75. User.find_by(username: id)
  76. end
  77. end
  78. def find_project(id)
  79. if id.is_a?(Integer) || id =~ /^\d+$/
  80. Project.find_by(id: id)
  81. elsif id.include?("/")
  82. Project.find_by_full_path(id)
  83. end
  84. end
  85. def find_project!(id)
  86. project = find_project(id)
  87. if can?(current_user, :read_project, project)
  88. project
  89. else
  90. not_found!('Project')
  91. end
  92. end
  93. def find_group(id)
  94. if id.to_s =~ /^\d+$/
  95. Group.find_by(id: id)
  96. else
  97. Group.find_by_full_path(id)
  98. end
  99. end
  100. def find_group!(id)
  101. group = find_group(id)
  102. if can?(current_user, :read_group, group)
  103. group
  104. else
  105. not_found!('Group')
  106. end
  107. end
  108. def find_namespace(id)
  109. if id.to_s =~ /^\d+$/
  110. Namespace.find_by(id: id)
  111. else
  112. Namespace.find_by_full_path(id)
  113. end
  114. end
  115. def find_namespace!(id)
  116. namespace = find_namespace(id)
  117. if can?(current_user, :read_namespace, namespace)
  118. namespace
  119. else
  120. not_found!('Namespace')
  121. end
  122. end
  123. def find_project_label(id)
  124. labels = available_labels_for(user_project)
  125. label = labels.find_by_id(id) || labels.find_by_title(id)
  126. label || not_found!('Label')
  127. end
  128. def find_project_issue(iid)
  129. IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
  130. end
  131. def find_project_merge_request(iid)
  132. MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
  133. end
  134. def find_project_commit(id)
  135. user_project.commit_by(oid: id)
  136. end
  137. def find_project_snippet(id)
  138. finder_params = { project: user_project }
  139. SnippetsFinder.new(current_user, finder_params).find(id)
  140. end
  141. def find_merge_request_with_access(iid, access_level = :read_merge_request)
  142. merge_request = user_project.merge_requests.find_by!(iid: iid)
  143. authorize! access_level, merge_request
  144. merge_request
  145. end
  146. def find_build!(id)
  147. user_project.builds.find(id.to_i)
  148. end
  149. def authenticate!
  150. unauthorized! unless current_user
  151. end
  152. def authenticate_non_get!
  153. authenticate! unless %w[GET HEAD].include?(route.request_method)
  154. end
  155. def authenticate_by_gitlab_shell_token!
  156. input = params['secret_token'].try(:chomp)
  157. unless Devise.secure_compare(secret_token, input)
  158. unauthorized!
  159. end
  160. end
  161. def authenticated_with_full_private_access!
  162. authenticate!
  163. forbidden! unless current_user.full_private_access?
  164. end
  165. def authenticated_as_admin!
  166. authenticate!
  167. forbidden! unless current_user.admin?
  168. end
  169. def authorize!(action, subject = :global)
  170. forbidden! unless can?(current_user, action, subject)
  171. end
  172. def authorize_push_project
  173. authorize! :push_code, user_project
  174. end
  175. def authorize_admin_project
  176. authorize! :admin_project, user_project
  177. end
  178. def authorize_read_builds!
  179. authorize! :read_build, user_project
  180. end
  181. def authorize_update_builds!
  182. authorize! :update_build, user_project
  183. end
  184. def require_gitlab_workhorse!
  185. unless env['HTTP_GITLAB_WORKHORSE'].present?
  186. forbidden!('Request should be executed via GitLab Workhorse')
  187. end
  188. end
  189. def require_pages_enabled!
  190. not_found! unless user_project.pages_available?
  191. end
  192. def require_pages_config_enabled!
  193. not_found! unless Gitlab.config.pages.enabled
  194. end
  195. def can?(object, action, subject = :global)
  196. Ability.allowed?(object, action, subject)
  197. end
  198. # Checks the occurrences of required attributes, each attribute must be present in the params hash
  199. # or a Bad Request error is invoked.
  200. #
  201. # Parameters:
  202. # keys (required) - A hash consisting of keys that must be present
  203. def required_attributes!(keys)
  204. keys.each do |key|
  205. bad_request!(key) unless params[key].present?
  206. end
  207. end
  208. def attributes_for_keys(keys, custom_params = nil)
  209. params_hash = custom_params || params
  210. attrs = {}
  211. keys.each do |key|
  212. if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
  213. attrs[key] = params_hash[key]
  214. end
  215. end
  216. ActionController::Parameters.new(attrs).permit!
  217. end
  218. def filter_by_iid(items, iid)
  219. items.where(iid: iid)
  220. end
  221. def filter_by_search(items, text)
  222. items.search(text)
  223. end
  224. # error helpers
  225. def forbidden!(reason = nil)
  226. message = ['403 Forbidden']
  227. message << " - #{reason}" if reason
  228. render_api_error!(message.join(' '), 403)
  229. end
  230. def bad_request!(attribute)
  231. message = ["400 (Bad request)"]
  232. message << "\"" + attribute.to_s + "\" not given" if attribute
  233. render_api_error!(message.join(' '), 400)
  234. end
  235. def not_found!(resource = nil)
  236. message = ["404"]
  237. message << resource if resource
  238. message << "Not Found"
  239. render_api_error!(message.join(' '), 404)
  240. end
  241. def unauthorized!
  242. render_api_error!('401 Unauthorized', 401)
  243. end
  244. def not_allowed!
  245. render_api_error!('405 Method Not Allowed', 405)
  246. end
  247. def conflict!(message = nil)
  248. render_api_error!(message || '409 Conflict', 409)
  249. end
  250. def file_to_large!
  251. render_api_error!('413 Request Entity Too Large', 413)
  252. end
  253. def not_modified!
  254. render_api_error!('304 Not Modified', 304)
  255. end
  256. def no_content!
  257. render_api_error!('204 No Content', 204)
  258. end
  259. def accepted!
  260. render_api_error!('202 Accepted', 202)
  261. end
  262. def render_validation_error!(model)
  263. if model.errors.any?
  264. render_api_error!(model.errors.messages || '400 Bad Request', 400)
  265. end
  266. end
  267. def render_spam_error!
  268. render_api_error!({ error: 'Spam detected' }, 400)
  269. end
  270. def render_api_error!(message, status)
  271. error!({ 'message' => message }, status, header)
  272. end
  273. def handle_api_exception(exception)
  274. if sentry_enabled? && report_exception?(exception)
  275. define_params_for_grape_middleware
  276. sentry_context
  277. Raven.capture_exception(exception, extra: params)
  278. end
  279. # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
  280. trace = exception.backtrace
  281. message = "\n#{exception.class} (#{exception.message}):\n"
  282. message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
  283. message << " " << trace.join("\n ")
  284. API.logger.add Logger::FATAL, message
  285. response_message =
  286. if Rails.env.test?
  287. message
  288. else
  289. '500 Internal Server Error'
  290. end
  291. rack_response({ 'message' => response_message }.to_json, 500)
  292. end
  293. # project helpers
  294. def reorder_projects(projects)
  295. projects.reorder(params[:order_by] => params[:sort])
  296. end
  297. def project_finder_params
  298. finder_params = {}
  299. finder_params[:owned] = true if params[:owned].present?
  300. finder_params[:non_public] = true if params[:membership].present?
  301. finder_params[:starred] = true if params[:starred].present?
  302. finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
  303. finder_params[:archived] = params[:archived]
  304. finder_params[:search] = params[:search] if params[:search]
  305. finder_params[:user] = params.delete(:user) if params[:user]
  306. finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
  307. finder_params
  308. end
  309. # file helpers
  310. def present_disk_file!(path, filename, content_type = 'application/octet-stream')
  311. filename ||= File.basename(path)
  312. header['Content-Disposition'] = "attachment; filename=#{filename}"
  313. header['Content-Transfer-Encoding'] = 'binary'
  314. content_type content_type
  315. # Support download acceleration
  316. case headers['X-Sendfile-Type']
  317. when 'X-Sendfile'
  318. header['X-Sendfile'] = path
  319. body
  320. else
  321. file path
  322. end
  323. end
  324. def present_carrierwave_file!(file, supports_direct_download: true)
  325. return not_found! unless file.exists?
  326. if file.file_storage?
  327. present_disk_file!(file.path, file.filename)
  328. elsif supports_direct_download && file.class.direct_download_enabled?
  329. redirect(file.url)
  330. else
  331. header(*Gitlab::Workhorse.send_url(file.url))
  332. status :ok
  333. body
  334. end
  335. end
  336. private
  337. # rubocop:disable Gitlab/ModuleWithInstanceVariables
  338. def initial_current_user
  339. return @initial_current_user if defined?(@initial_current_user)
  340. begin
  341. @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
  342. rescue Gitlab::Auth::UnauthorizedError
  343. unauthorized!
  344. end
  345. end
  346. # rubocop:enable Gitlab/ModuleWithInstanceVariables
  347. def sudo!
  348. return unless sudo_identifier
  349. unauthorized! unless initial_current_user
  350. unless initial_current_user.admin?
  351. forbidden!('Must be admin to use sudo')
  352. end
  353. unless access_token
  354. forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
  355. end
  356. validate_access_token!(scopes: [:sudo])
  357. sudoed_user = find_user(sudo_identifier)
  358. not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
  359. @current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables
  360. end
  361. def sudo_identifier
  362. @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
  363. end
  364. def secret_token
  365. Gitlab::Shell.secret_token
  366. end
  367. def send_git_blob(repository, blob)
  368. env['api.format'] = :txt
  369. content_type 'text/plain'
  370. header(*Gitlab::Workhorse.send_git_blob(repository, blob))
  371. end
  372. def send_git_archive(repository, **kwargs)
  373. header(*Gitlab::Workhorse.send_git_archive(repository, **kwargs))
  374. end
  375. def send_artifacts_entry(build, entry)
  376. header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
  377. end
  378. # The Grape Error Middleware only has access to `env` but not `params` nor
  379. # `request`. We workaround this by defining methods that returns the right
  380. # values.
  381. def define_params_for_grape_middleware
  382. self.define_singleton_method(:request) { Rack::Request.new(env) }
  383. self.define_singleton_method(:params) { request.params.symbolize_keys }
  384. end
  385. # We could get a Grape or a standard Ruby exception. We should only report anything that
  386. # is clearly an error.
  387. def report_exception?(exception)
  388. return true unless exception.respond_to?(:status)
  389. exception.status == 500
  390. end
  391. end
  392. end