PageRenderTime 53ms CodeModel.GetById 8ms app.highlight 41ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/api/helpers.rb

https://gitlab.com/vicvega/gitlab-ce
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