/lib/api/helpers.rb
Ruby | 465 lines | 342 code | 94 blank | 29 comment | 44 complexity | 4182813edb00c7b93a9bd73b36d97d0a MD5 | raw file
1module API
2 module Helpers
3 include Gitlab::Utils
4
5 PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
6 PRIVATE_TOKEN_PARAM = :private_token
7 SUDO_HEADER = "HTTP_SUDO"
8 SUDO_PARAM = :sudo
9
10 def private_token
11 params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
12 end
13
14 def warden
15 env['warden']
16 end
17
18 # Check the Rails session for valid authentication details
19 #
20 # Until CSRF protection is added to the API, disallow this method for
21 # state-changing endpoints
22 def find_user_from_warden
23 warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
24 end
25
26 def find_user_by_private_token
27 token = private_token
28 return nil unless token.present?
29
30 User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
31 end
32
33 def current_user
34 @current_user ||= find_user_by_private_token
35 @current_user ||= doorkeeper_guard
36 @current_user ||= find_user_from_warden
37
38 unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
39 return nil
40 end
41
42 identifier = sudo_identifier()
43
44 # If the sudo is the current user do nothing
45 if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
46 forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
47 @current_user = User.by_username_or_id(identifier)
48 not_found!("No user id or username for: #{identifier}") if @current_user.nil?
49 end
50
51 @current_user
52 end
53
54 def sudo_identifier
55 identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
56
57 # Regex for integers
58 if !!(identifier =~ /\A[0-9]+\z/)
59 identifier.to_i
60 else
61 identifier
62 end
63 end
64
65 def user_project
66 @project ||= find_project(params[:id])
67 end
68
69 def available_labels
70 @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
71 end
72
73 def find_project(id)
74 project = Project.find_with_namespace(id) || Project.find_by(id: id)
75
76 if can?(current_user, :read_project, project)
77 project
78 else
79 not_found!('Project')
80 end
81 end
82
83 def project_service
84 @project_service ||= begin
85 underscored_service = params[:service_slug].underscore
86
87 if Service.available_services_names.include?(underscored_service)
88 user_project.build_missing_services
89
90 service_method = "#{underscored_service}_service"
91
92 send_service(service_method)
93 end
94 end
95
96 @project_service || not_found!("Service")
97 end
98
99 def send_service(service_method)
100 user_project.send(service_method)
101 end
102
103 def service_attributes
104 @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
105 arr << hash[:name].to_sym
106 end
107 end
108
109 def find_group(id)
110 group = Group.find_by(path: id) || Group.find_by(id: id)
111
112 if can?(current_user, :read_group, group)
113 group
114 else
115 not_found!('Group')
116 end
117 end
118
119 def find_project_label(id)
120 label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
121 label || not_found!('Label')
122 end
123
124 def find_project_issue(id)
125 issue = user_project.issues.find(id)
126 not_found! unless can?(current_user, :read_issue, issue)
127 issue
128 end
129
130 def paginate(relation)
131 relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
132 add_pagination_headers(data)
133 end
134 end
135
136 def authenticate!
137 unauthorized! unless current_user
138 end
139
140 def authenticate_by_gitlab_shell_token!
141 input = params['secret_token'].try(:chomp)
142 unless Devise.secure_compare(secret_token, input)
143 unauthorized!
144 end
145 end
146
147 def authenticated_as_admin!
148 forbidden! unless current_user.is_admin?
149 end
150
151 def authorize!(action, subject = nil)
152 forbidden! unless can?(current_user, action, subject)
153 end
154
155 def authorize_push_project
156 authorize! :push_code, user_project
157 end
158
159 def authorize_admin_project
160 authorize! :admin_project, user_project
161 end
162
163 def require_gitlab_workhorse!
164 unless env['HTTP_GITLAB_WORKHORSE'].present?
165 forbidden!('Request should be executed via GitLab Workhorse')
166 end
167 end
168
169 def can?(object, action, subject)
170 Ability.allowed?(object, action, subject)
171 end
172
173 # Checks the occurrences of required attributes, each attribute must be present in the params hash
174 # or a Bad Request error is invoked.
175 #
176 # Parameters:
177 # keys (required) - A hash consisting of keys that must be present
178 def required_attributes!(keys)
179 keys.each do |key|
180 bad_request!(key) unless params[key].present?
181 end
182 end
183
184 def attributes_for_keys(keys, custom_params = nil)
185 params_hash = custom_params || params
186 attrs = {}
187 keys.each do |key|
188 if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
189 attrs[key] = params_hash[key]
190 end
191 end
192 ActionController::Parameters.new(attrs).permit!
193 end
194
195 # Helper method for validating all labels against its names
196 def validate_label_params(params)
197 errors = {}
198
199 params[:labels].to_s.split(',').each do |label_name|
200 label = available_labels.find_or_initialize_by(title: label_name.strip)
201 next if label.valid?
202
203 errors[label.title] = label.errors
204 end
205
206 errors
207 end
208
209 # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
210 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
211 #
212 # Parameters:
213 # keys (required) - An array consisting of elements that must be parseable as dates from the params hash
214 def datetime_attributes!(*keys)
215 keys.each do |key|
216 begin
217 params[key] = Time.xmlschema(params[key]) if params[key].present?
218 rescue ArgumentError
219 message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
220 render_api_error!(message, 400)
221 end
222 end
223 end
224
225 def issuable_order_by
226 if params["order_by"] == 'updated_at'
227 'updated_at'
228 else
229 'created_at'
230 end
231 end
232
233 def issuable_sort
234 if params["sort"] == 'asc'
235 :asc
236 else
237 :desc
238 end
239 end
240
241 def filter_by_iid(items, iid)
242 items.where(iid: iid)
243 end
244
245 # error helpers
246
247 def forbidden!(reason = nil)
248 message = ['403 Forbidden']
249 message << " - #{reason}" if reason
250 render_api_error!(message.join(' '), 403)
251 end
252
253 def bad_request!(attribute)
254 message = ["400 (Bad request)"]
255 message << "\"" + attribute.to_s + "\" not given"
256 render_api_error!(message.join(' '), 400)
257 end
258
259 def not_found!(resource = nil)
260 message = ["404"]
261 message << resource if resource
262 message << "Not Found"
263 render_api_error!(message.join(' '), 404)
264 end
265
266 def unauthorized!
267 render_api_error!('401 Unauthorized', 401)
268 end
269
270 def not_allowed!
271 render_api_error!('405 Method Not Allowed', 405)
272 end
273
274 def conflict!(message = nil)
275 render_api_error!(message || '409 Conflict', 409)
276 end
277
278 def file_to_large!
279 render_api_error!('413 Request Entity Too Large', 413)
280 end
281
282 def not_modified!
283 render_api_error!('304 Not Modified', 304)
284 end
285
286 def no_content!
287 render_api_error!('204 No Content', 204)
288 end
289
290 def render_validation_error!(model)
291 if model.errors.any?
292 render_api_error!(model.errors.messages || '400 Bad Request', 400)
293 end
294 end
295
296 def render_api_error!(message, status)
297 error!({ 'message' => message }, status)
298 end
299
300 def handle_api_exception(exception)
301 if sentry_enabled? && report_exception?(exception)
302 define_params_for_grape_middleware
303 sentry_context
304 Raven.capture_exception(exception)
305 end
306
307 # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
308 trace = exception.backtrace
309
310 message = "\n#{exception.class} (#{exception.message}):\n"
311 message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
312 message << " " << trace.join("\n ")
313
314 API.logger.add Logger::FATAL, message
315 rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
316 end
317
318 # Projects helpers
319
320 def filter_projects(projects)
321 # If the archived parameter is passed, limit results accordingly
322 if params[:archived].present?
323 projects = projects.where(archived: to_boolean(params[:archived]))
324 end
325
326 if params[:search].present?
327 projects = projects.search(params[:search])
328 end
329
330 if params[:visibility].present?
331 projects = projects.search_by_visibility(params[:visibility])
332 end
333
334 projects.reorder(project_order_by => project_sort)
335 end
336
337 def project_order_by
338 order_fields = %w(id name path created_at updated_at last_activity_at)
339
340 if order_fields.include?(params['order_by'])
341 params['order_by']
342 else
343 'created_at'
344 end
345 end
346
347 def project_sort
348 if params["sort"] == 'asc'
349 :asc
350 else
351 :desc
352 end
353 end
354
355 # file helpers
356
357 def uploaded_file(field, uploads_path)
358 if params[field]
359 bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
360 return params[field]
361 end
362
363 return nil unless params["#{field}.path"] && params["#{field}.name"]
364
365 # sanitize file paths
366 # this requires all paths to exist
367 required_attributes! %W(#{field}.path)
368 uploads_path = File.realpath(uploads_path)
369 file_path = File.realpath(params["#{field}.path"])
370 bad_request!('Bad file path') unless file_path.start_with?(uploads_path)
371
372 UploadedFile.new(
373 file_path,
374 params["#{field}.name"],
375 params["#{field}.type"] || 'application/octet-stream',
376 )
377 end
378
379 def present_file!(path, filename, content_type = 'application/octet-stream')
380 filename ||= File.basename(path)
381 header['Content-Disposition'] = "attachment; filename=#{filename}"
382 header['Content-Transfer-Encoding'] = 'binary'
383 content_type content_type
384
385 # Support download acceleration
386 case headers['X-Sendfile-Type']
387 when 'X-Sendfile'
388 header['X-Sendfile'] = path
389 body
390 else
391 file FileStreamer.new(path)
392 end
393 end
394
395 private
396
397 def add_pagination_headers(paginated_data)
398 header 'X-Total', paginated_data.total_count.to_s
399 header 'X-Total-Pages', paginated_data.total_pages.to_s
400 header 'X-Per-Page', paginated_data.limit_value.to_s
401 header 'X-Page', paginated_data.current_page.to_s
402 header 'X-Next-Page', paginated_data.next_page.to_s
403 header 'X-Prev-Page', paginated_data.prev_page.to_s
404 header 'Link', pagination_links(paginated_data)
405 end
406
407 def pagination_links(paginated_data)
408 request_url = request.url.split('?').first
409 request_params = params.clone
410 request_params[:per_page] = paginated_data.limit_value
411
412 links = []
413
414 request_params[:page] = paginated_data.current_page - 1
415 links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
416
417 request_params[:page] = paginated_data.current_page + 1
418 links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
419
420 request_params[:page] = 1
421 links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
422
423 request_params[:page] = paginated_data.total_pages
424 links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
425
426 links.join(', ')
427 end
428
429 def secret_token
430 Gitlab::Shell.secret_token
431 end
432
433 def send_git_blob(repository, blob)
434 env['api.format'] = :txt
435 content_type 'text/plain'
436 header(*Gitlab::Workhorse.send_git_blob(repository, blob))
437 end
438
439 def send_git_archive(repository, ref:, format:)
440 header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
441 end
442
443 def issue_entity(project)
444 if project.has_external_issue_tracker?
445 Entities::ExternalIssue
446 else
447 Entities::Issue
448 end
449 end
450
451 # The Grape Error Middleware only has access to env but no params. We workaround this by
452 # defining a method that returns the right value.
453 def define_params_for_grape_middleware
454 self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys }
455 end
456
457 # We could get a Grape or a standard Ruby exception. We should only report anything that
458 # is clearly an error.
459 def report_exception?(exception)
460 return true unless exception.respond_to?(:status)
461
462 exception.status == 500
463 end
464 end
465end