/lib/api/helpers.rb
Ruby | 434 lines | 330 code | 84 blank | 20 comment | 43 complexity | ea40ebd3a4dfedcbb023a3ec6d03c13b MD5 | raw file
1module API
2 module Helpers
3 PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
4 PRIVATE_TOKEN_PARAM = :private_token
5 SUDO_HEADER = "HTTP_SUDO"
6 SUDO_PARAM = :sudo
7
8 def parse_boolean(value)
9 [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
10 end
11
12 def find_user_by_private_token
13 token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
14 User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
15 end
16
17 def current_user
18 @current_user ||= (find_user_by_private_token || doorkeeper_guard)
19
20 unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
21 return nil
22 end
23
24 identifier = sudo_identifier()
25
26 # If the sudo is the current user do nothing
27 if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
28 render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
29 @current_user = User.by_username_or_id(identifier)
30 not_found!("No user id or username for: #{identifier}") if @current_user.nil?
31 end
32
33 @current_user
34 end
35
36 def sudo_identifier
37 identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
38
39 # Regex for integers
40 if !!(identifier =~ /\A[0-9]+\z/)
41 identifier.to_i
42 else
43 identifier
44 end
45 end
46
47 def user_project
48 @project ||= find_project(params[:id])
49 @project || not_found!("Project")
50 end
51
52 def find_project(id)
53 project = Project.find_with_namespace(id) || Project.find_by(id: id)
54
55 if project && can?(current_user, :read_project, project)
56 project
57 else
58 nil
59 end
60 end
61
62 def project_service
63 @project_service ||= begin
64 underscored_service = params[:service_slug].underscore
65
66 if Service.available_services_names.include?(underscored_service)
67 user_project.build_missing_services
68
69 service_method = "#{underscored_service}_service"
70
71 send_service(service_method)
72 end
73 end
74
75 @project_service || not_found!("Service")
76 end
77
78 def send_service(service_method)
79 user_project.send(service_method)
80 end
81
82 def service_attributes
83 @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
84 arr << hash[:name].to_sym
85 end
86 end
87
88 def find_group(id)
89 begin
90 group = Group.find(id)
91 rescue ActiveRecord::RecordNotFound
92 group = Group.find_by!(path: id)
93 end
94
95 if can?(current_user, :read_group, group)
96 group
97 else
98 not_found!('Group')
99 end
100 end
101
102 def find_project_label(id)
103 label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
104 label || not_found!('Label')
105 end
106
107 def find_project_issue(id)
108 issue = user_project.issues.find(id)
109 not_found! unless can?(current_user, :read_issue, issue)
110 issue
111 end
112
113 def paginate(relation)
114 relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
115 add_pagination_headers(data)
116 end
117 end
118
119 def authenticate!
120 unauthorized! unless current_user
121 end
122
123 def authenticate_by_gitlab_shell_token!
124 input = params['secret_token'].try(:chomp)
125 unless Devise.secure_compare(secret_token, input)
126 unauthorized!
127 end
128 end
129
130 def authenticated_as_admin!
131 forbidden! unless current_user.is_admin?
132 end
133
134 def authorize!(action, subject)
135 forbidden! unless abilities.allowed?(current_user, action, subject)
136 end
137
138 def authorize_push_project
139 authorize! :push_code, user_project
140 end
141
142 def authorize_admin_project
143 authorize! :admin_project, user_project
144 end
145
146 def require_gitlab_workhorse!
147 unless env['HTTP_GITLAB_WORKHORSE'].present?
148 forbidden!('Request should be executed via GitLab Workhorse')
149 end
150 end
151
152 def can?(object, action, subject)
153 abilities.allowed?(object, action, subject)
154 end
155
156 # Checks the occurrences of required attributes, each attribute must be present in the params hash
157 # or a Bad Request error is invoked.
158 #
159 # Parameters:
160 # keys (required) - A hash consisting of keys that must be present
161 def required_attributes!(keys)
162 keys.each do |key|
163 bad_request!(key) unless params[key].present?
164 end
165 end
166
167 def attributes_for_keys(keys, custom_params = nil)
168 params_hash = custom_params || params
169 attrs = {}
170 keys.each do |key|
171 if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
172 attrs[key] = params_hash[key]
173 end
174 end
175 ActionController::Parameters.new(attrs).permit!
176 end
177
178 # Helper method for validating all labels against its names
179 def validate_label_params(params)
180 errors = {}
181
182 if params[:labels].present?
183 params[:labels].split(',').each do |label_name|
184 label = user_project.labels.create_with(
185 color: Label::DEFAULT_COLOR).find_or_initialize_by(
186 title: label_name.strip)
187
188 if label.invalid?
189 errors[label.title] = label.errors
190 end
191 end
192 end
193
194 errors
195 end
196
197 def validate_access_level?(level)
198 Gitlab::Access.options_with_owner.values.include? level.to_i
199 end
200
201 # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
202 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
203 #
204 # Parameters:
205 # keys (required) - An array consisting of elements that must be parseable as dates from the params hash
206 def datetime_attributes!(*keys)
207 keys.each do |key|
208 begin
209 params[key] = Time.xmlschema(params[key]) if params[key].present?
210 rescue ArgumentError
211 message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
212 render_api_error!(message, 400)
213 end
214 end
215 end
216
217 def issuable_order_by
218 if params["order_by"] == 'updated_at'
219 'updated_at'
220 else
221 'created_at'
222 end
223 end
224
225 def issuable_sort
226 if params["sort"] == 'asc'
227 :asc
228 else
229 :desc
230 end
231 end
232
233 def filter_by_iid(items, iid)
234 items.where(iid: iid)
235 end
236
237 # error helpers
238
239 def forbidden!(reason = nil)
240 message = ['403 Forbidden']
241 message << " - #{reason}" if reason
242 render_api_error!(message.join(' '), 403)
243 end
244
245 def bad_request!(attribute)
246 message = ["400 (Bad request)"]
247 message << "\"" + attribute.to_s + "\" not given"
248 render_api_error!(message.join(' '), 400)
249 end
250
251 def not_found!(resource = nil)
252 message = ["404"]
253 message << resource if resource
254 message << "Not Found"
255 render_api_error!(message.join(' '), 404)
256 end
257
258 def unauthorized!
259 render_api_error!('401 Unauthorized', 401)
260 end
261
262 def not_allowed!
263 render_api_error!('405 Method Not Allowed', 405)
264 end
265
266 def conflict!(message = nil)
267 render_api_error!(message || '409 Conflict', 409)
268 end
269
270 def file_to_large!
271 render_api_error!('413 Request Entity Too Large', 413)
272 end
273
274 def not_modified!
275 render_api_error!('304 Not Modified', 304)
276 end
277
278 def render_validation_error!(model)
279 if model.errors.any?
280 render_api_error!(model.errors.messages || '400 Bad Request', 400)
281 end
282 end
283
284 def render_api_error!(message, status)
285 error!({ 'message' => message }, status)
286 end
287
288 # Projects helpers
289
290 def filter_projects(projects)
291 # If the archived parameter is passed, limit results accordingly
292 if params[:archived].present?
293 projects = projects.where(archived: parse_boolean(params[:archived]))
294 end
295
296 if params[:search].present?
297 projects = projects.search(params[:search])
298 end
299
300 if params[:visibility].present?
301 projects = projects.search_by_visibility(params[:visibility])
302 end
303
304 projects.reorder(project_order_by => project_sort)
305 end
306
307 def project_order_by
308 order_fields = %w(id name path created_at updated_at last_activity_at)
309
310 if order_fields.include?(params['order_by'])
311 params['order_by']
312 else
313 'created_at'
314 end
315 end
316
317 def project_sort
318 if params["sort"] == 'asc'
319 :asc
320 else
321 :desc
322 end
323 end
324
325 # file helpers
326
327 def uploaded_file(field, uploads_path)
328 if params[field]
329 bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
330 return params[field]
331 end
332
333 return nil unless params["#{field}.path"] && params["#{field}.name"]
334
335 # sanitize file paths
336 # this requires all paths to exist
337 required_attributes! %W(#{field}.path)
338 uploads_path = File.realpath(uploads_path)
339 file_path = File.realpath(params["#{field}.path"])
340 bad_request!('Bad file path') unless file_path.start_with?(uploads_path)
341
342 UploadedFile.new(
343 file_path,
344 params["#{field}.name"],
345 params["#{field}.type"] || 'application/octet-stream',
346 )
347 end
348
349 def present_file!(path, filename, content_type = 'application/octet-stream')
350 filename ||= File.basename(path)
351 header['Content-Disposition'] = "attachment; filename=#{filename}"
352 header['Content-Transfer-Encoding'] = 'binary'
353 content_type content_type
354
355 # Support download acceleration
356 case headers['X-Sendfile-Type']
357 when 'X-Sendfile'
358 header['X-Sendfile'] = path
359 body
360 else
361 file FileStreamer.new(path)
362 end
363 end
364
365 private
366
367 def add_pagination_headers(paginated_data)
368 header 'X-Total', paginated_data.total_count.to_s
369 header 'X-Total-Pages', paginated_data.total_pages.to_s
370 header 'X-Per-Page', paginated_data.limit_value.to_s
371 header 'X-Page', paginated_data.current_page.to_s
372 header 'X-Next-Page', paginated_data.next_page.to_s
373 header 'X-Prev-Page', paginated_data.prev_page.to_s
374 header 'Link', pagination_links(paginated_data)
375 end
376
377 def pagination_links(paginated_data)
378 request_url = request.url.split('?').first
379 request_params = params.clone
380 request_params[:per_page] = paginated_data.limit_value
381
382 links = []
383
384 request_params[:page] = paginated_data.current_page - 1
385 links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
386
387 request_params[:page] = paginated_data.current_page + 1
388 links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
389
390 request_params[:page] = 1
391 links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
392
393 request_params[:page] = paginated_data.total_pages
394 links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
395
396 links.join(', ')
397 end
398
399 def abilities
400 @abilities ||= begin
401 abilities = Six.new
402 abilities << Ability
403 abilities
404 end
405 end
406
407 def secret_token
408 File.read(Gitlab.config.gitlab_shell.secret_file).chomp
409 end
410
411 def handle_member_errors(errors)
412 error!(errors[:access_level], 422) if errors[:access_level].any?
413 not_found!(errors)
414 end
415
416 def send_git_blob(repository, blob)
417 env['api.format'] = :txt
418 content_type 'text/plain'
419 header(*Gitlab::Workhorse.send_git_blob(repository, blob))
420 end
421
422 def send_git_archive(repository, ref:, format:)
423 header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
424 end
425
426 def issue_entity(project)
427 if project.has_external_issue_tracker?
428 Entities::ExternalIssue
429 else
430 Entities::Issue
431 end
432 end
433 end
434end