a5034fd895593a8c9e1573e41730bce1f015dc54
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   include ArvadosApiClientHelper
3   include ApplicationHelper
4
5   respond_to :html, :json, :js
6   protect_from_forgery
7
8   ERROR_ACTIONS = [:render_error, :render_not_found]
9
10   around_filter :thread_clear
11   around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS
12   around_filter :thread_with_optional_api_token
13   before_filter :check_user_agreements, except: ERROR_ACTIONS
14   before_filter :check_user_notifications, except: ERROR_ACTIONS
15   before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS
16   theme :select_theme
17
18   begin
19     rescue_from Exception,
20     :with => :render_exception
21     rescue_from ActiveRecord::RecordNotFound,
22     :with => :render_not_found
23     rescue_from ActionController::RoutingError,
24     :with => :render_not_found
25     rescue_from ActionController::UnknownController,
26     :with => :render_not_found
27     rescue_from ::AbstractController::ActionNotFound,
28     :with => :render_not_found
29   end
30
31   def unprocessable(message=nil)
32     @errors ||= []
33
34     @errors << message if message
35     render_error status: 422
36   end
37
38   def render_error(opts)
39     opts = {status: 500}.merge opts
40     respond_to do |f|
41       # json must come before html here, so it gets used as the
42       # default format when js is requested by the client. This lets
43       # ajax:error callback parse the response correctly, even though
44       # the browser can't.
45       f.json { render opts.merge(json: {success: false, errors: @errors}) }
46       f.html { render opts.merge(controller: 'application', action: 'error') }
47     end
48   end
49
50   def render_exception(e)
51     logger.error e.inspect
52     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
53     if @object.andand.errors.andand.full_messages.andand.any?
54       @errors = @object.errors.full_messages
55     else
56       @errors = [e.to_s]
57     end
58     self.render_error status: 422
59   end
60
61   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
62     logger.error e.inspect
63     @errors = ["Path not found"]
64     self.render_error status: 404
65   end
66
67   def find_objects_for_index
68     @limit ||= 200
69     if params[:limit]
70       @limit = params[:limit].to_i
71     end
72
73     @offset ||= 0
74     if params[:offset]
75       @offset = params[:offset].to_i
76     end
77
78     @filters ||= []
79     if params[:filters]
80       filters = params[:filters]
81       if filters.is_a? String
82         filters = Oj.load filters
83       end
84       @filters += filters
85     end
86
87     @objects ||= model_class
88     @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
89   end
90
91   helper_method :next_page_offset
92   def next_page_offset
93     if @objects.respond_to?(:result_offset) and
94         @objects.respond_to?(:result_limit) and
95         @objects.respond_to?(:items_available)
96       next_offset = @objects.result_offset + @objects.result_limit
97       if next_offset < @objects.items_available
98         next_offset
99       else
100         nil
101       end
102     end
103   end
104
105   def index
106     find_objects_for_index if !@objects
107     respond_to do |f|
108       f.json { render json: @objects }
109       f.html { render }
110       f.js { render }
111     end
112   end
113
114   def show
115     if !@object
116       return render_not_found("object not found")
117     end
118     respond_to do |f|
119       f.json { render json: @object.attributes.merge(href: url_for(@object)) }
120       f.html {
121         if request.method.in? ['GET', 'HEAD']
122           render
123         else
124           redirect_to params[:return_to] || @object
125         end
126       }
127       f.js { render }
128     end
129   end
130
131   def choose
132     params[:limit] ||= 20
133     find_objects_for_index if !@objects
134     respond_to do |f|
135       if params[:partial]
136         f.json {
137           render json: {
138             content: render_to_string(partial: "choose_rows.html",
139                                       formats: [:html],
140                                       locals: {
141                                         multiple: params[:multiple]
142                                       }),
143             next_page_href: @next_page_href
144           }
145         }
146       end
147       f.js {
148         render partial: 'choose', locals: {multiple: params[:multiple]}
149       }
150     end
151   end
152
153   def render_content
154     if !@object
155       return render_not_found("object not found")
156     end
157   end
158
159   def new
160     @object = model_class.new
161   end
162
163   def update
164     @updates ||= params[@object.resource_param_name.to_sym]
165     @updates.keys.each do |attr|
166       if @object.send(attr).is_a? Hash
167         if @updates[attr].is_a? String
168           @updates[attr] = Oj.load @updates[attr]
169         end
170         if params[:merge] || params["merge_#{attr}".to_sym]
171           # Merge provided Hash with current Hash, instead of
172           # replacing.
173           @updates[attr] = @object.send(attr).with_indifferent_access.
174             deep_merge(@updates[attr].with_indifferent_access)
175         end
176       end
177     end
178     if @object.update_attributes @updates
179       show
180     else
181       self.render_error status: 422
182     end
183   end
184
185   def create
186     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
187     @new_resource_attrs ||= {}
188     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
189     @object ||= model_class.new @new_resource_attrs
190     @object.save!
191     show
192   end
193
194   # Clone the given object, merging any attribute values supplied as
195   # with a create action.
196   def copy
197     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
198     @new_resource_attrs ||= {}
199     @object = @object.dup
200     @object.update_attributes @new_resource_attrs
201     if not @new_resource_attrs[:name] and @object.respond_to? :name
202       if @object.name and @object.name != ''
203         @object.name = "Copy of #{@object.name}"
204       else
205         @object.name = "Copy of unnamed #{@object.class_for_display.downcase}"
206       end
207     end
208     @object.save!
209     show
210   end
211
212   def destroy
213     if @object.destroy
214       respond_to do |f|
215         f.json { render json: @object }
216         f.html {
217           redirect_to(params[:return_to] || :back)
218         }
219         f.js { render }
220       end
221     else
222       self.render_error status: 422
223     end
224   end
225
226   def current_user
227     if Thread.current[:arvados_api_token]
228       Thread.current[:user] ||= User.current
229     else
230       logger.error "No API token in Thread"
231       return nil
232     end
233   end
234
235   def model_class
236     controller_name.classify.constantize
237   end
238
239   def breadcrumb_page_name
240     (@breadcrumb_page_name ||
241      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
242      action_name)
243   end
244
245   def index_pane_list
246     %w(Recent)
247   end
248
249   def show_pane_list
250     %w(Attributes Advanced)
251   end
252
253   protected
254
255   def redirect_to_login
256     respond_to do |f|
257       f.html {
258         if request.method.in? ['GET', 'HEAD']
259           redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
260         else
261           flash[:error] = "Either you are not logged in, or your session has timed out. I can't automatically log you in and re-attempt this request."
262           redirect_to :back
263         end
264       }
265       f.json {
266         @errors = ['You do not seem to be logged in. You did not supply an API token with this request, and your session (if any) has timed out.']
267         self.render_error status: 422
268       }
269     end
270     false  # For convenience to return from callbacks
271   end
272
273   def using_specific_api_token(api_token)
274     start_values = {}
275     [:arvados_api_token, :user].each do |key|
276       start_values[key] = Thread.current[key]
277     end
278     Thread.current[:arvados_api_token] = api_token
279     Thread.current[:user] = nil
280     begin
281       yield
282     ensure
283       start_values.each_key { |key| Thread.current[key] = start_values[key] }
284     end
285   end
286
287   def find_object_by_uuid
288     if params[:id] and params[:id].match /\D/
289       params[:uuid] = params.delete :id
290     end
291     if not model_class
292       @object = nil
293     elsif params[:uuid].is_a? String
294       if params[:uuid].empty?
295         @object = nil
296       else
297         if (model_class != Link and
298             resource_class_for_uuid(params[:uuid]) == Link)
299           @name_link = Link.find(params[:uuid])
300           @object = model_class.find(@name_link.head_uuid)
301         else
302           @object = model_class.find(params[:uuid])
303         end
304       end
305     else
306       @object = model_class.where(uuid: params[:uuid]).first
307     end
308   end
309
310   def thread_clear
311     Thread.current[:arvados_api_token] = nil
312     Thread.current[:user] = nil
313     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
314     yield
315     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
316   end
317
318   def thread_with_api_token(login_optional = false)
319     begin
320       try_redirect_to_login = true
321       if params[:api_token]
322         try_redirect_to_login = false
323         Thread.current[:arvados_api_token] = params[:api_token]
324         # Before copying the token into session[], do a simple API
325         # call to verify its authenticity.
326         if verify_api_token
327           session[:arvados_api_token] = params[:api_token]
328           if !request.format.json? and request.method.in? ['GET', 'HEAD']
329             # Repeat this request with api_token in the (new) session
330             # cookie instead of the query string.  This prevents API
331             # tokens from appearing in (and being inadvisedly copied
332             # and pasted from) browser Location bars.
333             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
334           else
335             yield
336           end
337         else
338           @errors = ['Invalid API token']
339           self.render_error status: 401
340         end
341       elsif session[:arvados_api_token]
342         # In this case, the token must have already verified at some
343         # point, but it might have been revoked since.  We'll try
344         # using it, and catch the exception if it doesn't work.
345         try_redirect_to_login = false
346         Thread.current[:arvados_api_token] = session[:arvados_api_token]
347         begin
348           yield
349         rescue ArvadosApiClient::NotLoggedInException
350           try_redirect_to_login = true
351         end
352       else
353         logger.debug "No token received, session is #{session.inspect}"
354       end
355       if try_redirect_to_login
356         unless login_optional
357           redirect_to_login
358         else
359           # login is optional for this route so go on to the regular controller
360           Thread.current[:arvados_api_token] = nil
361           yield
362         end
363       end
364     ensure
365       # Remove token in case this Thread is used for anything else.
366       Thread.current[:arvados_api_token] = nil
367     end
368   end
369
370   def thread_with_mandatory_api_token
371     thread_with_api_token do
372       yield
373     end
374   end
375
376   # This runs after thread_with_mandatory_api_token in the filter chain.
377   def thread_with_optional_api_token
378     if Thread.current[:arvados_api_token]
379       # We are already inside thread_with_mandatory_api_token.
380       yield
381     else
382       # We skipped thread_with_mandatory_api_token. Use the optional version.
383       thread_with_api_token(true) do
384         yield
385       end
386     end
387   end
388
389   def verify_api_token
390     begin
391       Link.where(uuid: 'just-verifying-my-api-token')
392       true
393     rescue ArvadosApiClient::NotLoggedInException
394       false
395     end
396   end
397
398   def ensure_current_user_is_admin
399     unless current_user and current_user.is_admin
400       @errors = ['Permission denied']
401       self.render_error status: 401
402     end
403   end
404
405   def check_user_agreements
406     if current_user && !current_user.is_active && current_user.is_invited
407       signatures = UserAgreement.signatures
408       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
409       @required_user_agreements = UserAgreement.all.map do |ua|
410         if not @signed_ua_uuids.index ua.uuid
411           Collection.find(ua.uuid)
412         end
413       end.compact
414       if @required_user_agreements.empty?
415         # No agreements to sign. Perhaps we just need to ask?
416         current_user.activate
417         if !current_user.is_active
418           logger.warn "#{current_user.uuid.inspect}: " +
419             "No user agreements to sign, but activate failed!"
420         end
421       end
422       if !current_user.is_active
423         render 'user_agreements/index'
424       end
425     end
426     true
427   end
428
429   def select_theme
430     return Rails.configuration.arvados_theme
431   end
432
433   @@notification_tests = []
434
435   @@notification_tests.push lambda { |controller, current_user|
436     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
437       return nil
438     end
439     return lambda { |view|
440       view.render partial: 'notifications/ssh_key_notification'
441     }
442   }
443
444   #@@notification_tests.push lambda { |controller, current_user|
445   #  Job.limit(1).where(created_by: current_user.uuid).each do
446   #    return nil
447   #  end
448   #  return lambda { |view|
449   #    view.render partial: 'notifications/jobs_notification'
450   #  }
451   #}
452
453   @@notification_tests.push lambda { |controller, current_user|
454     Collection.limit(1).where(created_by: current_user.uuid).each do
455       return nil
456     end
457     return lambda { |view|
458       view.render partial: 'notifications/collections_notification'
459     }
460   }
461
462   @@notification_tests.push lambda { |controller, current_user|
463     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
464       return nil
465     end
466     return lambda { |view|
467       view.render partial: 'notifications/pipelines_notification'
468     }
469   }
470
471   def check_user_notifications
472     @notification_count = 0
473     @notifications = []
474
475     if current_user
476       @showallalerts = false
477       @@notification_tests.each do |t|
478         a = t.call(self, current_user)
479         if a
480           @notification_count += 1
481           @notifications.push a
482         end
483       end
484     end
485
486     if @notification_count == 0
487       @notification_count = ''
488     end
489   end
490
491   helper_method :my_folders
492   def my_folders
493     return @my_folders if @my_folders
494     @my_folders = []
495     root_of = {}
496     Group.filter([['group_class','=','folder']]).each do |g|
497       root_of[g.uuid] = g.owner_uuid
498       @my_folders << g
499     end
500     done = false
501     while not done
502       done = true
503       root_of = root_of.each_with_object({}) do |(child, parent), h|
504         if root_of[parent]
505           h[child] = root_of[parent]
506           done = false
507         else
508           h[child] = parent
509         end
510       end
511     end
512     @my_folders = @my_folders.select do |g|
513       root_of[g.uuid] == current_user.uuid
514     end
515   end
516
517   helper_method :recent_jobs_and_pipelines
518   def recent_jobs_and_pipelines
519     (Job.limit(10) | PipelineInstance.limit(10)).
520       sort_by do |x|
521       x.finished_at || x.started_at || x.created_at rescue x.created_at
522     end
523   end
524
525   helper_method :get_object
526   def get_object uuid
527     if @get_object.nil? and @objects
528       @get_object = @objects.each_with_object({}) do |object, h|
529         h[object.uuid] = object
530       end
531     end
532     @get_object ||= {}
533     @get_object[uuid]
534   end
535 end