helpers.rb 11.7 KB
Newer Older
1
module API
2
  module Helpers
3 4
    PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
    PRIVATE_TOKEN_PARAM = :private_token
5
    SUDO_HEADER = "HTTP_SUDO"
6 7
    SUDO_PARAM = :sudo

8 9 10 11
    def parse_boolean(value)
      [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
12
    def current_user
13
      private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
Valery Sizov's avatar
Valery Sizov committed
14
      @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
15 16 17 18 19

      unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
        return nil
      end

20
      identifier = sudo_identifier()
21

22
      # If the sudo is the current user do nothing
23
      if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
24
        render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
Nihad Abbasov's avatar
Nihad Abbasov committed
25
        @current_user = User.by_username_or_id(identifier)
Nihad Abbasov's avatar
Nihad Abbasov committed
26
        not_found!("No user id or username for: #{identifier}") if @current_user.nil?
27
      end
28

29 30 31
      @current_user
    end

32
    def sudo_identifier
33
      identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
34

35
      # Regex for integers
36
      if !!(identifier =~ /^[0-9]+$/)
37 38 39 40
        identifier.to_i
      else
        identifier
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
41 42
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
43
    def user_project
44
      @project ||= find_project(params[:id])
45
      @project || not_found!("Project")
46 47
    end

48
    def find_project(id)
49
      project = Project.find_with_namespace(id) || Project.find_by(id: id)
50 51 52

      if project && can?(current_user, :read_project, project)
        project
53
      else
54
        nil
55
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
56 57
    end

Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
58 59 60 61 62 63 64 65
    def project_service
      @project_service ||= begin
        underscored_service = params[:service_slug].underscore

        if Service.available_services_names.include?(underscored_service)
          user_project.build_missing_services

          service_method = "#{underscored_service}_service"
66

Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
67 68 69
          send_service(service_method)
        end
      end
70

Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
71 72 73 74 75 76 77 78 79 80 81 82 83
      @project_service || not_found!("Service")
    end

    def send_service(service_method)
      user_project.send(service_method)
    end

    def service_attributes
      @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
        arr << hash[:name].to_sym
      end
    end

84 85 86 87 88 89 90 91 92 93
    def find_group(id)
      begin
        group = Group.find(id)
      rescue ActiveRecord::RecordNotFound
        group = Group.find_by!(path: id)
      end

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

98 99 100 101 102 103 104 105 106 107 108
    def find_project_label(id)
      label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
      label || not_found!('Label')
    end

    def find_project_issue(id)
      issue = user_project.issues.find(id)
      not_found! unless can?(current_user, :read_issue, issue)
      issue
    end

109
    def paginate(relation)
110 111 112
      relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
        add_pagination_headers(data)
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
113 114
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
115
    def authenticate!
116
      unauthorized! unless current_user
Nihad Abbasov's avatar
Nihad Abbasov committed
117
    end
randx's avatar
randx committed
118

119
    def authenticate_by_gitlab_shell_token!
120 121 122 123
      input = params['secret_token'].try(:chomp)
      unless Devise.secure_compare(secret_token, input)
        unauthorized!
      end
124 125
    end

126 127 128 129
    def authenticated_as_admin!
      forbidden! unless current_user.is_admin?
    end

130
    def authorize!(action, subject)
131
      forbidden! unless abilities.allowed?(current_user, action, subject)
randx's avatar
randx committed
132 133
    end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
134 135 136 137
    def authorize_push_project
      authorize! :push_code, user_project
    end

138 139 140 141
    def authorize_admin_project
      authorize! :admin_project, user_project
    end

142
    def require_gitlab_workhorse!
143
      unless env['HTTP_GITLAB_WORKHORSE'].present?
144 145 146 147
        forbidden!('Request should be executed via GitLab Workhorse')
      end
    end

148 149 150 151
    def can?(object, action, subject)
      abilities.allowed?(object, action, subject)
    end

152 153 154 155 156 157 158 159 160 161 162
    # 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

Valery Sizov's avatar
Valery Sizov committed
163
    def attributes_for_keys(keys, custom_params = nil)
164
      params_hash = custom_params || params
Alex Denisov's avatar
Alex Denisov committed
165 166
      attrs = {}
      keys.each do |key|
167 168
        if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
          attrs[key] = params_hash[key]
169
        end
Alex Denisov's avatar
Alex Denisov committed
170
      end
171
      ActionController::Parameters.new(attrs).permit!
Alex Denisov's avatar
Alex Denisov committed
172 173
    end

174 175
    # Helper method for validating all labels against its names
    def validate_label_params(params)
176 177
      errors = {}

178 179 180 181 182
      if params[:labels].present?
        params[:labels].split(',').each do |label_name|
          label = user_project.labels.create_with(
            color: Label::DEFAULT_COLOR).find_or_initialize_by(
              title: label_name.strip)
183

184
          if label.invalid?
185
            errors[label.title] = label.errors
186 187 188
          end
        end
      end
189 190

      errors
191 192
    end

193 194 195 196
    def validate_access_level?(level)
      Gitlab::Access.options_with_owner.values.include? level.to_i
    end

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
    # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
    # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
    #
    # Parameters:
    #   keys (required) - An array consisting of elements that must be parseable as dates from the params hash
    def datetime_attributes!(*keys)
      keys.each do |key|
        begin
          params[key] = Time.xmlschema(params[key]) if params[key].present?
        rescue ArgumentError
          message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
          render_api_error!(message, 400)
        end
      end
    end

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    def issuable_order_by
      if params["order_by"] == 'updated_at'
        'updated_at'
      else
        'created_at'
      end
    end

    def issuable_sort
      if params["sort"] == 'asc'
        :asc
      else
        :desc
      end
    end

229 230 231 232
    def filter_by_iid(items, iid)
      items.where(iid: iid)
    end

233 234
    # error helpers

235 236 237 238
    def forbidden!(reason = nil)
      message = ['403 Forbidden']
      message << " - #{reason}" if reason
      render_api_error!(message.join(' '), 403)
239 240
    end

241 242 243 244 245 246
    def bad_request!(attribute)
      message = ["400 (Bad request)"]
      message << "\"" + attribute.to_s + "\" not given"
      render_api_error!(message.join(' '), 400)
    end

247 248 249 250
    def not_found!(resource = nil)
      message = ["404"]
      message << resource if resource
      message << "Not Found"
Alex Denisov's avatar
Alex Denisov committed
251
      render_api_error!(message.join(' '), 404)
252 253 254
    end

    def unauthorized!
Alex Denisov's avatar
Alex Denisov committed
255
      render_api_error!('401 Unauthorized', 401)
256 257 258
    end

    def not_allowed!
259 260 261 262 263 264 265
      render_api_error!('405 Method Not Allowed', 405)
    end

    def conflict!(message = nil)
      render_api_error!(message || '409 Conflict', 409)
    end

266 267 268 269
    def file_to_large!
      render_api_error!('413 Request Entity Too Large', 413)
    end

270
    def not_modified!
271
      render_api_error!('304 Not Modified', 304)
272 273
    end

274
    def render_validation_error!(model)
275
      if model.errors.any?
276 277
        render_api_error!(model.errors.messages || '400 Bad Request', 400)
      end
Alex Denisov's avatar
Alex Denisov committed
278 279 280
    end

    def render_api_error!(message, status)
281
      error!({ 'message' => message }, status)
282 283
    end

Valery Sizov's avatar
Valery Sizov committed
284 285 286 287 288 289 290 291 292 293 294 295
    # Projects helpers

    def filter_projects(projects)
      # If the archived parameter is passed, limit results accordingly
      if params[:archived].present?
        projects = projects.where(archived: parse_boolean(params[:archived]))
      end

      if params[:search].present?
        projects = projects.search(params[:search])
      end

296 297
      if params[:visibility].present?
        projects = projects.search_by_visibility(params[:visibility])
298 299
      end

300
      projects.reorder(project_order_by => project_sort)
Valery Sizov's avatar
Valery Sizov committed
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    end

    def project_order_by
      order_fields = %w(id name path created_at updated_at last_activity_at)

      if order_fields.include?(params['order_by'])
        params['order_by']
      else
        'created_at'
      end
    end

    def project_sort
      if params["sort"] == 'asc'
        :asc
      else
        :desc
      end
    end

321 322
    # file helpers

323
    def uploaded_file(field, uploads_path)
324 325 326 327
      if params[field]
        bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
        return params[field]
      end
328

329 330
      return nil unless params["#{field}.path"] && params["#{field}.name"]

331
      # sanitize file paths
332 333
      # this requires all paths to exist
      required_attributes! %W(#{field}.path)
334
      uploads_path = File.realpath(uploads_path)
335
      file_path = File.realpath(params["#{field}.path"])
336 337 338 339
      bad_request!('Bad file path') unless file_path.start_with?(uploads_path)

      UploadedFile.new(
        file_path,
340 341
        params["#{field}.name"],
        params["#{field}.type"] || 'application/octet-stream',
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
      )
    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
        file FileStreamer.new(path)
      end
    end

361
    private
randx's avatar
randx committed
362

363 364 365 366 367 368 369 370 371 372 373
    def add_pagination_headers(paginated_data)
      header 'X-Total',       paginated_data.total_count.to_s
      header 'X-Total-Pages', paginated_data.total_pages.to_s
      header 'X-Per-Page',    paginated_data.limit_value.to_s
      header 'X-Page',        paginated_data.current_page.to_s
      header 'X-Next-Page',   paginated_data.next_page.to_s
      header 'X-Prev-Page',   paginated_data.prev_page.to_s
      header 'Link',          pagination_links(paginated_data)
    end

    def pagination_links(paginated_data)
374
      request_url = request.url.split('?').first
375 376
      request_params = params.clone
      request_params[:per_page] = paginated_data.limit_value
377 378

      links = []
379 380 381 382 383 384 385 386 387 388 389 390

      request_params[:page] = paginated_data.current_page - 1
      links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?

      request_params[:page] = paginated_data.current_page + 1
      links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?

      request_params[:page] = 1
      links << %(<#{request_url}?#{request_params.to_query}>; rel="first")

      request_params[:page] = paginated_data.total_pages
      links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
391

392
      links.join(', ')
393 394
    end

randx's avatar
randx committed
395 396
    def abilities
      @abilities ||= begin
397 398 399 400
                       abilities = Six.new
                       abilities << Ability
                       abilities
                     end
randx's avatar
randx committed
401
    end
402 403

    def secret_token
404
      File.read(Gitlab.config.gitlab_shell.secret_file).chomp
405
    end
Vinnie Okada's avatar
Vinnie Okada committed
406 407 408 409 410

    def handle_member_errors(errors)
      error!(errors[:access_level], 422) if errors[:access_level].any?
      not_found!(errors)
    end
411 412 413 414

    def send_git_blob(repository, blob)
      env['api.format'] = :txt
      content_type 'text/plain'
Douwe Maan's avatar
Douwe Maan committed
415
      header(*Gitlab::Workhorse.send_git_blob(repository, blob))
416 417 418
    end

    def send_git_archive(repository, ref:, format:)
Douwe Maan's avatar
Douwe Maan committed
419
      header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
420
    end
421 422 423 424 425 426 427 428

    def issue_entity(project)
      if project.has_external_issue_tracker?
        Entities::ExternalIssue
      else
        Entities::Issue
      end
    end
Nihad Abbasov's avatar
Nihad Abbasov committed
429 430
  end
end