helpers.rb 13.2 KB
Newer Older
1 2
module API
  module Helpers
F
Felipe Artur 已提交
3
    include Gitlab::Utils
4
    include Helpers::Pagination
F
Felipe Artur 已提交
5

D
Douwe Maan 已提交
6
    SUDO_HEADER = "HTTP_SUDO".freeze
7 8
    SUDO_PARAM = :sudo

9 10 11 12 13
    def declared_params(options = {})
      options = { include_parent_namespaces: false }.merge(options)
      declared(params, options).to_h.symbolize_keys
    end

14 15
    def check_unmodified_since!(last_modified)
      if_unmodified_since = Time.parse(headers['If-Unmodified-Since']) rescue nil
16

R
Robert Schilling 已提交
17
      if if_unmodified_since && last_modified && last_modified > if_unmodified_since
18 19 20 21
        render_api_error!('412 Precondition Failed', 412)
      end
    end

22 23 24 25
    def destroy_conditionally!(resource, last_updated: nil)
      last_updated ||= resource.updated_at

      check_unmodified_since!(last_updated)
26 27 28 29 30 31 32 33 34

      status 204
      if block_given?
        yield resource
      else
        resource.destroy
      end
    end

35
    # rubocop:disable Gitlab/ModuleWithInstanceVariables
36 37 38 39
    # 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
40
    def current_user
41
      return @current_user if defined?(@current_user)
42

43
      @current_user = initial_current_user
44

45 46
      Gitlab::I18n.locale = @current_user&.preferred_language

47
      sudo!
48

D
Douwe Maan 已提交
49 50
      validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?

51 52
      @current_user
    end
53
    # rubocop:enable Gitlab/ModuleWithInstanceVariables
54

55 56
    def sudo?
      initial_current_user != current_user
57 58
    end

59 60 61 62
    def user_namespace
      @user_namespace ||= find_namespace!(params[:id])
    end

F
Felipe Artur 已提交
63 64 65 66
    def user_group
      @group ||= find_group!(params[:id])
    end

67
    def user_project
68
      @project ||= find_project!(params[:id])
69 70
    end

B
blackst0ne 已提交
71
    def wiki_page
72
      page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
B
blackst0ne 已提交
73 74 75 76

      page || not_found!('Wiki Page')
    end

77 78 79 80
    def available_labels
      @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
    end

81 82 83 84 85 86 87 88
    def find_user(id)
      if id =~ /^\d+$/
        User.find_by(id: id)
      else
        User.find_by(username: id)
      end
    end

89
    def find_project(id)
90 91 92
      if id =~ /^\d+$/
        Project.find_by(id: id)
      else
93
        Project.find_by_full_path(id)
94 95 96 97 98
      end
    end

    def find_project!(id)
      project = find_project(id)
99

100
      if can?(current_user, :read_project, project)
101 102
        project
      else
103
        not_found!('Project')
104 105 106 107
      end
    end

    def find_group(id)
108
      if id.to_s =~ /^\d+$/
109 110
        Group.find_by(id: id)
      else
111
        Group.find_by_full_path(id)
112 113 114 115 116
      end
    end

    def find_group!(id)
      group = find_group(id)
117 118 119 120 121 122 123 124

      if can?(current_user, :read_group, group)
        group
      else
        not_found!('Group')
      end
    end

125 126 127 128 129 130 131 132 133 134 135
    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)

136
      if can?(current_user, :read_namespace, namespace)
137 138 139 140 141 142
        namespace
      else
        not_found!('Namespace')
      end
    end

143
    def find_project_label(id)
144
      label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
145 146 147
      label || not_found!('Label')
    end

148 149
    def find_project_issue(iid)
      IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
150 151
    end

152 153
    def find_project_merge_request(iid)
      MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
154 155
    end

156
    def find_project_snippet(id)
157 158
      finder_params = { project: user_project }
      SnippetsFinder.new(current_user, finder_params).execute.find(id)
159 160
    end

161 162
    def find_merge_request_with_access(iid, access_level = :read_merge_request)
      merge_request = user_project.merge_requests.find_by!(iid: iid)
163 164 165 166
      authorize! access_level, merge_request
      merge_request
    end

167 168 169 170
    def find_build!(id)
      user_project.builds.find(id.to_i)
    end

171
    def authenticate!
172
      unauthorized! unless current_user
173 174
    end

175
    def authenticate_non_get!
176
      authenticate! unless %w[GET HEAD].include?(route.request_method)
177 178
    end

179 180 181 182 183 184 185
    def authenticate_by_gitlab_shell_token!
      input = params['secret_token'].try(:chomp)
      unless Devise.secure_compare(secret_token, input)
        unauthorized!
      end
    end

186 187 188 189 190
    def authenticated_with_full_private_access!
      authenticate!
      forbidden! unless current_user.full_private_access?
    end

191
    def authenticated_as_admin!
192
      authenticate!
B
blackst0ne 已提交
193
      forbidden! unless current_user.admin?
194 195
    end

196
    def authorize!(action, subject = :global)
197
      forbidden! unless can?(current_user, action, subject)
198 199 200 201 202 203 204 205 206 207
    end

    def authorize_push_project
      authorize! :push_code, user_project
    end

    def authorize_admin_project
      authorize! :admin_project, user_project
    end

208 209 210 211 212 213 214 215
    def authorize_read_builds!
      authorize! :read_build, user_project
    end

    def authorize_update_builds!
      authorize! :update_build, user_project
    end

216 217 218 219 220 221
    def require_gitlab_workhorse!
      unless env['HTTP_GITLAB_WORKHORSE'].present?
        forbidden!('Request should be executed via GitLab Workhorse')
      end
    end

222 223 224 225
    def require_pages_enabled!
      not_found! unless user_project.pages_available?
    end

226 227 228 229
    def require_pages_config_enabled!
      not_found! unless Gitlab.config.pages.enabled
    end

230
    def can?(object, action, subject = :global)
H
http://jneen.net/ 已提交
231
      Ability.allowed?(object, action, subject)
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
    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|
249
        if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
250 251 252 253 254 255 256 257 258 259
          attrs[key] = params_hash[key]
        end
      end
      ActionController::Parameters.new(attrs).permit!
    end

    def filter_by_iid(items, iid)
      items.where(iid: iid)
    end

260 261 262 263
    def filter_by_search(items, text)
      items.search(text)
    end

264 265 266 267 268 269 270 271 272 273
    # 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)"]
274
      message << "\"" + attribute.to_s + "\" not given" if attribute
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
      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

305 306 307 308
    def no_content!
      render_api_error!('204 No Content', 204)
    end

309 310 311 312
    def accepted!
      render_api_error!('202 Accepted', 202)
    end

313 314 315 316 317 318
    def render_validation_error!(model)
      if model.errors.any?
        render_api_error!(model.errors.messages || '400 Bad Request', 400)
      end
    end

319 320 321 322
    def render_spam_error!
      render_api_error!({ error: 'Spam detected' }, 400)
    end

323
    def render_api_error!(message, status)
K
Kamil Trzcinski 已提交
324
      error!({ 'message' => message }, status, header)
325 326
    end

S
Stan Hu 已提交
327 328 329 330
    def handle_api_exception(exception)
      if sentry_enabled? && report_exception?(exception)
        define_params_for_grape_middleware
        sentry_context
331
        Raven.capture_exception(exception, extra: params)
S
Stan Hu 已提交
332 333 334 335 336 337 338 339 340 341
      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
342 343 344 345 346 347 348 349 350

      response_message =
        if Rails.env.test?
          message
        else
          '500 Internal Server Error'
        end

      rack_response({ 'message' => response_message }.to_json, 500)
S
Stan Hu 已提交
351 352
    end

M
Markus Koller 已提交
353
    # project helpers
354

T
Toon Claes 已提交
355
    def reorder_projects(projects)
R
Robert Schilling 已提交
356
      projects.reorder(params[:order_by] => params[:sort])
357 358
    end

359 360
    def project_finder_params
      finder_params = {}
T
Toon Claes 已提交
361
      finder_params[:owned] = true if params[:owned].present?
362 363 364 365 366
      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]
V
vanadium23 已提交
367
      finder_params[:user] = params.delete(:user) if params[:user]
368
      finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
369 370 371
      finder_params
    end

372 373 374 375
    # file helpers

    def uploaded_file(field, uploads_path)
      if params[field]
Z
Zeger-Jan van de Weg 已提交
376
        bad_request!("#{field} is not a file") unless params[field][:filename]
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
        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"],
392
        params["#{field}.type"] || 'application/octet-stream'
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
      )
    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
R
Robert Schilling 已提交
408
        file path
409 410 411
      end
    end

K
Kamil Trzcinski 已提交
412 413
    def present_artifacts!(artifacts_file)
      return not_found! unless artifacts_file.exists?
V
vanadium23 已提交
414

K
Kamil Trzcinski 已提交
415 416 417 418 419 420 421
      if artifacts_file.file_storage?
        present_file!(artifacts_file.path, artifacts_file.filename)
      else
        redirect_to(artifacts_file.url)
      end
    end

422 423
    private

424
    # rubocop:disable Gitlab/ModuleWithInstanceVariables
425 426 427
    def initial_current_user
      return @initial_current_user if defined?(@initial_current_user)

428
      begin
D
Douwe Maan 已提交
429
        @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
F
Francisco Lopez 已提交
430
      rescue Gitlab::Auth::UnauthorizedError
431
        unauthorized!
432 433
      end
    end
434
    # rubocop:enable Gitlab/ModuleWithInstanceVariables
435 436 437

    def sudo!
      return unless sudo_identifier
D
Douwe Maan 已提交
438

D
Douwe Maan 已提交
439
      unauthorized! unless initial_current_user
440

B
blackst0ne 已提交
441
      unless initial_current_user.admin?
442 443 444
        forbidden!('Must be admin to use sudo')
      end

D
Douwe Maan 已提交
445 446
      unless access_token
        forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
447 448
      end

D
Douwe Maan 已提交
449 450
      validate_access_token!(scopes: [:sudo])

451
      sudoed_user = find_user(sudo_identifier)
D
Douwe Maan 已提交
452
      not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
453

454
      @current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables
455 456 457
    end

    def sudo_identifier
458
      @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
459 460
    end

461
    def secret_token
462
      Gitlab::Shell.secret_token
463 464
    end

465 466 467
    def send_git_blob(repository, blob)
      env['api.format'] = :txt
      content_type 'text/plain'
D
Douwe Maan 已提交
468
      header(*Gitlab::Workhorse.send_git_blob(repository, blob))
469 470 471
    end

    def send_git_archive(repository, ref:, format:)
D
Douwe Maan 已提交
472
      header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
473
    end
474

475 476 477 478
    def send_artifacts_entry(build, entry)
      header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
    end

479 480 481
    # 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.
S
Stan Hu 已提交
482
    def define_params_for_grape_middleware
483 484
      self.define_singleton_method(:request) { Rack::Request.new(env) }
      self.define_singleton_method(:params) { request.params.symbolize_keys }
S
Stan Hu 已提交
485 486 487 488 489 490 491 492 493
    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
494 495
  end
end