BigW Consortium Gitlab

helpers.rb 12.7 KB
Newer Older
1 2
module API
  module Helpers
3
    include Gitlab::Utils
4
    include Helpers::Pagination
5

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

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
    def current_user
36
      return @current_user if defined?(@current_user)
37

38
      @current_user = initial_current_user
39

40 41
      Gitlab::I18n.locale = @current_user&.preferred_language

42
      sudo!
43

Douwe Maan committed
44 45
      validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?

46 47 48
      @current_user
    end

49 50
    def sudo?
      initial_current_user != current_user
51 52
    end

53 54 55 56
    def user_namespace
      @user_namespace ||= find_namespace!(params[:id])
    end

57 58 59 60
    def user_group
      @group ||= find_group!(params[:id])
    end

61
    def user_project
62
      @project ||= find_project!(params[:id])
63 64
    end

65 66 67 68 69 70
    def wiki_page
      page = user_project.wiki.find_page(params[:slug])

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

71 72 73 74
    def available_labels
      @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
    end

75 76 77 78 79 80 81 82
    def find_user(id)
      if id =~ /^\d+$/
        User.find_by(id: id)
      else
        User.find_by(username: id)
      end
    end

83
    def find_project(id)
84 85 86
      if id =~ /^\d+$/
        Project.find_by(id: id)
      else
87
        Project.find_by_full_path(id)
88 89 90 91 92
      end
    end

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

94
      if can?(current_user, :read_project, project)
95 96
        project
      else
97
        not_found!('Project')
98 99 100 101
      end
    end

    def find_group(id)
102
      if id.to_s =~ /^\d+$/
103 104
        Group.find_by(id: id)
      else
105
        Group.find_by_full_path(id)
106 107 108 109 110
      end
    end

    def find_group!(id)
      group = find_group(id)
111 112 113 114 115 116 117 118

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

119 120 121 122 123 124 125 126 127 128 129
    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)

130
      if can?(current_user, :read_namespace, namespace)
131 132 133 134 135 136
        namespace
      else
        not_found!('Namespace')
      end
    end

137
    def find_project_label(id)
138
      label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
139 140 141
      label || not_found!('Label')
    end

142 143
    def find_project_issue(iid)
      IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
144 145
    end

146 147
    def find_project_merge_request(iid)
      MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
148 149
    end

150
    def find_project_snippet(id)
151 152
      finder_params = { project: user_project }
      SnippetsFinder.new(current_user, finder_params).execute.find(id)
153 154
    end

155 156
    def find_merge_request_with_access(iid, access_level = :read_merge_request)
      merge_request = user_project.merge_requests.find_by!(iid: iid)
157 158 159 160
      authorize! access_level, merge_request
      merge_request
    end

161 162 163 164
    def find_build!(id)
      user_project.builds.find(id.to_i)
    end

165
    def authenticate!
166
      unauthorized! unless current_user
167 168
    end

169
    def authenticate_non_get!
170
      authenticate! unless %w[GET HEAD].include?(route.request_method)
171 172
    end

173 174 175 176 177 178 179
    def authenticate_by_gitlab_shell_token!
      input = params['secret_token'].try(:chomp)
      unless Devise.secure_compare(secret_token, input)
        unauthorized!
      end
    end

180 181 182 183 184
    def authenticated_with_full_private_access!
      authenticate!
      forbidden! unless current_user.full_private_access?
    end

185
    def authenticated_as_admin!
186
      authenticate!
187
      forbidden! unless current_user.admin?
188 189
    end

190
    def authorize!(action, subject = :global)
191
      forbidden! unless can?(current_user, action, subject)
192 193 194 195 196 197 198 199 200 201
    end

    def authorize_push_project
      authorize! :push_code, user_project
    end

    def authorize_admin_project
      authorize! :admin_project, user_project
    end

202 203 204 205 206 207 208 209
    def authorize_read_builds!
      authorize! :read_build, user_project
    end

    def authorize_update_builds!
      authorize! :update_build, user_project
    end

210 211 212 213 214 215
    def require_gitlab_workhorse!
      unless env['HTTP_GITLAB_WORKHORSE'].present?
        forbidden!('Request should be executed via GitLab Workhorse')
      end
    end

216 217 218 219
    def require_pages_enabled!
      not_found! unless user_project.pages_available?
    end

220 221 222 223
    def require_pages_config_enabled!
      not_found! unless Gitlab.config.pages.enabled
    end

224
    def can?(object, action, subject = :global)
225
      Ability.allowed?(object, action, subject)
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
    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|
243
        if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
244 245 246 247 248 249 250 251 252 253
          attrs[key] = params_hash[key]
        end
      end
      ActionController::Parameters.new(attrs).permit!
    end

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

254 255 256 257
    def filter_by_search(items, text)
      items.search(text)
    end

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

299 300 301 302
    def no_content!
      render_api_error!('204 No Content', 204)
    end

303 304 305 306
    def accepted!
      render_api_error!('202 Accepted', 202)
    end

307 308 309 310 311 312
    def render_validation_error!(model)
      if model.errors.any?
        render_api_error!(model.errors.messages || '400 Bad Request', 400)
      end
    end

313 314 315 316
    def render_spam_error!
      render_api_error!({ error: 'Spam detected' }, 400)
    end

317
    def render_api_error!(message, status)
Kamil Trzcinski committed
318
      error!({ 'message' => message }, status, header)
319 320
    end

321 322 323 324
    def handle_api_exception(exception)
      if sentry_enabled? && report_exception?(exception)
        define_params_for_grape_middleware
        sentry_context
325
        Raven.capture_exception(exception, extra: params)
326 327 328 329 330 331 332 333 334 335
      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
336 337 338 339 340 341 342 343 344

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

      rack_response({ 'message' => response_message }.to_json, 500)
345 346
    end

347
    # project helpers
348

349
    def reorder_projects(projects)
350
      projects.reorder(params[:order_by] => params[:sort])
351 352
    end

353 354
    def project_finder_params
      finder_params = {}
355
      finder_params[:owned] = true if params[:owned].present?
356 357 358 359 360
      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]
vanadium23 committed
361
      finder_params[:user] = params.delete(:user) if params[:user]
362
      finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
363 364 365
      finder_params
    end

366 367 368 369
    # file helpers

    def uploaded_file(field, uploads_path)
      if params[field]
370
        bad_request!("#{field} is not a file") unless params[field][:filename]
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
        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"],
386
        params["#{field}.type"] || 'application/octet-stream'
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
      )
    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
402
        file path
403 404 405
      end
    end

Kamil Trzcinski committed
406 407
    def present_artifacts!(artifacts_file)
      return not_found! unless artifacts_file.exists?
vanadium23 committed
408

Kamil Trzcinski committed
409 410 411 412 413 414 415
      if artifacts_file.file_storage?
        present_file!(artifacts_file.path, artifacts_file.filename)
      else
        redirect_to(artifacts_file.url)
      end
    end

416 417
    private

418 419 420
    def initial_current_user
      return @initial_current_user if defined?(@initial_current_user)

421
      begin
Douwe Maan committed
422
        @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
423
      rescue Gitlab::Auth::UnauthorizedError
424
        unauthorized!
425 426 427 428 429
      end
    end

    def sudo!
      return unless sudo_identifier
Douwe Maan committed
430

431
      unauthorized! unless initial_current_user
432

433
      unless initial_current_user.admin?
434 435 436
        forbidden!('Must be admin to use sudo')
      end

Douwe Maan committed
437 438
      unless access_token
        forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
439 440
      end

Douwe Maan committed
441 442
      validate_access_token!(scopes: [:sudo])

443
      sudoed_user = find_user(sudo_identifier)
444
      not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
Douwe Maan committed
445 446

      @current_user = sudoed_user
447 448 449
    end

    def sudo_identifier
450
      @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
451 452
    end

453
    def secret_token
454
      Gitlab::Shell.secret_token
455 456
    end

457 458 459
    def send_git_blob(repository, blob)
      env['api.format'] = :txt
      content_type 'text/plain'
Douwe Maan committed
460
      header(*Gitlab::Workhorse.send_git_blob(repository, blob))
461 462 463
    end

    def send_git_archive(repository, ref:, format:)
Douwe Maan committed
464
      header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
465
    end
466

467 468 469 470
    def send_artifacts_entry(build, entry)
      header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
    end

471 472 473
    # 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.
474
    def define_params_for_grape_middleware
475 476
      self.define_singleton_method(:request) { Rack::Request.new(env) }
      self.define_singleton_method(:params) { request.params.symbolize_keys }
477 478 479 480 481 482 483 484 485
    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
486 487
  end
end