BigW Consortium Gitlab

helpers.rb 13.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
    SUDO_PARAM = :sudo
8
    API_USER_ENV = 'gitlab.api.user'.freeze
9

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

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

18
      if if_unmodified_since && last_modified && last_modified > if_unmodified_since
19 20 21 22
        render_api_error!('412 Precondition Failed', 412)
      end
    end

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

      check_unmodified_since!(last_updated)
27 28

      status 204
29

30 31 32 33 34 35 36
      if block_given?
        yield resource
      else
        resource.destroy
      end
    end

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

45
      @current_user = initial_current_user
46

47 48
      Gitlab::I18n.locale = @current_user&.preferred_language

49
      sudo!
50

Douwe Maan committed
51 52
      validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?

53 54
      save_current_user_in_env(@current_user) if @current_user

55 56
      @current_user
    end
57
    # rubocop:enable Gitlab/ModuleWithInstanceVariables
58

59 60 61 62
    def save_current_user_in_env(user)
      env[API_USER_ENV] = { user_id: user.id, username: user.username }
    end

63 64
    def sudo?
      initial_current_user != current_user
65 66
    end

67 68 69 70
    def user_namespace
      @user_namespace ||= find_namespace!(params[:id])
    end

71 72 73 74
    def user_group
      @group ||= find_group!(params[:id])
    end

75
    def user_project
76
      @project ||= find_project!(params[:id])
77 78
    end

79
    def wiki_page
80
      page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
81 82 83 84

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

85 86 87 88 89 90 91 92 93
    def available_labels_for(label_parent)
      search_params =
        if label_parent.is_a?(Project)
          { project_id: label_parent.id }
        else
          { group_id: label_parent.id, only_group_labels: true }
        end

      LabelsFinder.new(current_user, search_params).execute
94 95
    end

96 97 98 99 100 101 102 103
    def find_user(id)
      if id =~ /^\d+$/
        User.find_by(id: id)
      else
        User.find_by(username: id)
      end
    end

104
    def find_project(id)
105 106 107
      if id =~ /^\d+$/
        Project.find_by(id: id)
      else
108
        Project.find_by_full_path(id)
109 110 111 112 113
      end
    end

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

115
      if can?(current_user, :read_project, project)
116 117
        project
      else
118
        not_found!('Project')
119 120 121 122
      end
    end

    def find_group(id)
123
      if id.to_s =~ /^\d+$/
124 125
        Group.find_by(id: id)
      else
126
        Group.find_by_full_path(id)
127 128 129 130 131
      end
    end

    def find_group!(id)
      group = find_group(id)
132 133 134 135 136 137 138 139

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

140 141 142 143 144 145 146 147 148 149 150
    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)

151
      if can?(current_user, :read_namespace, namespace)
152 153 154 155 156 157
        namespace
      else
        not_found!('Namespace')
      end
    end

158
    def find_project_label(id)
159 160 161
      labels = available_labels_for(user_project)
      label = labels.find_by_id(id) || labels.find_by_title(id)

162 163 164
      label || not_found!('Label')
    end

165 166
    def find_project_issue(iid)
      IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
167 168
    end

169 170
    def find_project_merge_request(iid)
      MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
171 172
    end

173
    def find_project_snippet(id)
174 175
      finder_params = { project: user_project }
      SnippetsFinder.new(current_user, finder_params).execute.find(id)
176 177
    end

178 179
    def find_merge_request_with_access(iid, access_level = :read_merge_request)
      merge_request = user_project.merge_requests.find_by!(iid: iid)
180 181 182 183
      authorize! access_level, merge_request
      merge_request
    end

184 185 186 187
    def find_build!(id)
      user_project.builds.find(id.to_i)
    end

188
    def authenticate!
189
      unauthorized! unless current_user
190 191
    end

192
    def authenticate_non_get!
193
      authenticate! unless %w[GET HEAD].include?(route.request_method)
194 195
    end

196 197 198 199 200 201 202
    def authenticate_by_gitlab_shell_token!
      input = params['secret_token'].try(:chomp)
      unless Devise.secure_compare(secret_token, input)
        unauthorized!
      end
    end

203 204 205 206 207
    def authenticated_with_full_private_access!
      authenticate!
      forbidden! unless current_user.full_private_access?
    end

208
    def authenticated_as_admin!
209
      authenticate!
210
      forbidden! unless current_user.admin?
211 212
    end

213
    def authorize!(action, subject = :global)
214
      forbidden! unless can?(current_user, action, subject)
215 216 217 218 219 220 221 222 223 224
    end

    def authorize_push_project
      authorize! :push_code, user_project
    end

    def authorize_admin_project
      authorize! :admin_project, user_project
    end

225 226 227 228 229 230 231 232
    def authorize_read_builds!
      authorize! :read_build, user_project
    end

    def authorize_update_builds!
      authorize! :update_build, user_project
    end

233 234 235 236 237 238
    def require_gitlab_workhorse!
      unless env['HTTP_GITLAB_WORKHORSE'].present?
        forbidden!('Request should be executed via GitLab Workhorse')
      end
    end

239 240 241 242
    def require_pages_enabled!
      not_found! unless user_project.pages_available?
    end

243 244 245 246
    def require_pages_config_enabled!
      not_found! unless Gitlab.config.pages.enabled
    end

247
    def can?(object, action, subject = :global)
248
      Ability.allowed?(object, action, subject)
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
    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|
266
        if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
267 268 269 270 271 272 273 274 275 276
          attrs[key] = params_hash[key]
        end
      end
      ActionController::Parameters.new(attrs).permit!
    end

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

277 278 279 280
    def filter_by_search(items, text)
      items.search(text)
    end

281 282 283 284 285 286 287 288 289 290
    # 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)"]
291
      message << "\"" + attribute.to_s + "\" not given" if attribute
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
      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

322 323 324 325
    def no_content!
      render_api_error!('204 No Content', 204)
    end

326 327 328 329
    def accepted!
      render_api_error!('202 Accepted', 202)
    end

330 331 332 333 334 335
    def render_validation_error!(model)
      if model.errors.any?
        render_api_error!(model.errors.messages || '400 Bad Request', 400)
      end
    end

336 337 338 339
    def render_spam_error!
      render_api_error!({ error: 'Spam detected' }, 400)
    end

340
    def render_api_error!(message, status)
Kamil Trzcinski committed
341
      error!({ 'message' => message }, status, header)
342 343
    end

344 345 346 347
    def handle_api_exception(exception)
      if sentry_enabled? && report_exception?(exception)
        define_params_for_grape_middleware
        sentry_context
348
        Raven.capture_exception(exception, extra: params)
349 350 351 352 353 354 355 356 357 358
      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
359 360 361 362 363 364 365 366 367

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

      rack_response({ 'message' => response_message }.to_json, 500)
368 369
    end

370
    # project helpers
371

372
    def reorder_projects(projects)
373
      projects.reorder(params[:order_by] => params[:sort])
374 375
    end

376 377
    def project_finder_params
      finder_params = {}
378
      finder_params[:owned] = true if params[:owned].present?
379 380 381 382 383
      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
384
      finder_params[:user] = params.delete(:user) if params[:user]
385
      finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
386 387 388
      finder_params
    end

389 390 391 392
    # file helpers

    def uploaded_file(field, uploads_path)
      if params[field]
393
        bad_request!("#{field} is not a file") unless params[field][:filename]
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
        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"],
409
        params["#{field}.type"] || 'application/octet-stream'
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
      )
    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
425
        file path
426 427 428
      end
    end

Kamil Trzcinski committed
429 430
    def present_artifacts!(artifacts_file)
      return not_found! unless artifacts_file.exists?
vanadium23 committed
431

Kamil Trzcinski committed
432 433 434 435 436 437 438
      if artifacts_file.file_storage?
        present_file!(artifacts_file.path, artifacts_file.filename)
      else
        redirect_to(artifacts_file.url)
      end
    end

439 440
    private

441
    # rubocop:disable Gitlab/ModuleWithInstanceVariables
442 443 444
    def initial_current_user
      return @initial_current_user if defined?(@initial_current_user)

445
      begin
Douwe Maan committed
446
        @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
447
      rescue Gitlab::Auth::UnauthorizedError
448
        unauthorized!
449 450
      end
    end
451
    # rubocop:enable Gitlab/ModuleWithInstanceVariables
452 453 454

    def sudo!
      return unless sudo_identifier
Douwe Maan committed
455

456
      unauthorized! unless initial_current_user
457

458
      unless initial_current_user.admin?
459 460 461
        forbidden!('Must be admin to use sudo')
      end

Douwe Maan committed
462 463
      unless access_token
        forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
464 465
      end

Douwe Maan committed
466 467
      validate_access_token!(scopes: [:sudo])

468
      sudoed_user = find_user(sudo_identifier)
469
      not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
470

471
      @current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables
472 473 474
    end

    def sudo_identifier
475
      @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
476 477
    end

478
    def secret_token
479
      Gitlab::Shell.secret_token
480 481
    end

482 483 484
    def send_git_blob(repository, blob)
      env['api.format'] = :txt
      content_type 'text/plain'
Douwe Maan committed
485
      header(*Gitlab::Workhorse.send_git_blob(repository, blob))
486 487 488
    end

    def send_git_archive(repository, ref:, format:)
Douwe Maan committed
489
      header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
490
    end
491

492 493 494 495
    def send_artifacts_entry(build, entry)
      header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
    end

496 497 498
    # 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.
499
    def define_params_for_grape_middleware
500 501
      self.define_singleton_method(:request) { Rack::Request.new(env) }
      self.define_singleton_method(:params) { request.params.symbolize_keys }
502 503 504 505 506 507 508 509 510
    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
511 512
  end
end