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