Merge branch 'master' into 2640-folder-api
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   respond_to :html, :json, :js
3   protect_from_forgery
4
5   ERROR_ACTIONS = [:render_error, :render_not_found]
6
7   around_filter :thread_clear
8   around_filter(:thread_with_mandatory_api_token,
9                 except: [:index, :show] + ERROR_ACTIONS)
10   around_filter :thread_with_optional_api_token
11   before_filter :check_user_agreements, except: ERROR_ACTIONS
12   before_filter :check_user_notifications, except: ERROR_ACTIONS
13   around_filter :using_reader_tokens, only: [:index, :show]
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     if params[:limit]
68       limit = params[:limit].to_i
69     else
70       limit = 200
71     end
72
73     if params[:offset]
74       offset = params[:offset].to_i
75     else
76       offset = 0
77     end
78
79     @objects ||= model_class.limit(limit).offset(offset).all
80     respond_to do |f|
81       f.json { render json: @objects }
82       f.html { render }
83       f.js { render }
84     end
85   end
86
87   def show
88     if !@object
89       return render_not_found("object not found")
90     end
91     respond_to do |f|
92       f.json { render json: @object }
93       f.html {
94         if request.method == 'GET'
95           render
96         else
97           redirect_to params[:return_to] || @object
98         end
99       }
100       f.js { render }
101     end
102   end
103
104   def render_content
105     if !@object
106       return render_not_found("object not found")
107     end
108   end
109
110   def new
111     @object = model_class.new
112   end
113
114   def update
115     updates = params[@object.class.to_s.underscore.singularize.to_sym]
116     updates.keys.each do |attr|
117       if @object.send(attr).is_a? Hash
118         if updates[attr].is_a? String
119           updates[attr] = Oj.load updates[attr]
120         end
121         if params[:merge] || params["merge_#{attr}".to_sym]
122           # Merge provided Hash with current Hash, instead of
123           # replacing.
124           updates[attr] = @object.send(attr).with_indifferent_access.
125             deep_merge(updates[attr].with_indifferent_access)
126         end
127       end
128     end
129     if @object.update_attributes updates
130       show
131     else
132       self.render_error status: 422
133     end
134   end
135
136   def create
137     @object ||= model_class.new params[model_class.to_s.underscore.singularize]
138     @object.save!
139
140     respond_to do |f|
141       f.json { render json: @object }
142       f.html {
143         redirect_to(params[:return_to] || @object)
144       }
145       f.js { render }
146     end
147   end
148
149   def destroy
150     if @object.destroy
151       respond_to do |f|
152         f.json { render json: @object }
153         f.html {
154           redirect_to(params[:return_to] || :back)
155         }
156         f.js { render }
157       end
158     else
159       self.render_error status: 422
160     end
161   end
162
163   def current_user
164     if Thread.current[:arvados_api_token]
165       Thread.current[:user] ||= User.current
166     else
167       logger.error "No API token in Thread"
168       return nil
169     end
170   end
171
172   def model_class
173     controller_name.classify.constantize
174   end
175
176   def breadcrumb_page_name
177     (@breadcrumb_page_name ||
178      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
179      action_name)
180   end
181
182   def index_pane_list
183     %w(Recent)
184   end
185
186   def show_pane_list
187     %w(Attributes Metadata JSON API)
188   end
189
190   protected
191
192   def redirect_to_login
193     respond_to do |f|
194       f.html {
195         if request.method == 'GET'
196           redirect_to $arvados_api_client.arvados_login_url(return_to: request.url)
197         else
198           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."
199           redirect_to :back
200         end
201       }
202       f.json {
203         @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.']
204         self.render_error status: 422
205       }
206     end
207     false  # For convenience to return from callbacks
208   end
209
210   def using_reader_tokens(login_optional=false)
211     if params[:reader_tokens].is_a?(Array) and params[:reader_tokens].any?
212       Thread.current[:reader_tokens] = params[:reader_tokens]
213     end
214     begin
215       yield
216     rescue ArvadosApiClient::NotLoggedInException
217       if login_optional
218         raise
219       else
220         return redirect_to_login
221       end
222     ensure
223       Thread.current[:reader_tokens] = nil
224     end
225   end
226
227   def using_specific_api_token(api_token)
228     start_values = {}
229     [:arvados_api_token, :user].each do |key|
230       start_values[key] = Thread.current[key]
231     end
232     Thread.current[:arvados_api_token] = api_token
233     Thread.current[:user] = nil
234     begin
235       yield
236     ensure
237       start_values.each_key { |key| Thread.current[key] = start_values[key] }
238     end
239   end
240
241   def find_object_by_uuid
242     if params[:id] and params[:id].match /\D/
243       params[:uuid] = params.delete :id
244     end
245     if params[:uuid].is_a? String
246       @object = model_class.find(params[:uuid])
247     else
248       @object = model_class.where(uuid: params[:uuid]).first
249     end
250   end
251
252   def thread_clear
253     Thread.current[:arvados_api_token] = nil
254     Thread.current[:user] = nil
255     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
256     yield
257     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
258   end
259
260   def thread_with_api_token(login_optional = false)
261     begin
262       try_redirect_to_login = true
263       if params[:api_token]
264         try_redirect_to_login = false
265         Thread.current[:arvados_api_token] = params[:api_token]
266         # Before copying the token into session[], do a simple API
267         # call to verify its authenticity.
268         if verify_api_token
269           session[:arvados_api_token] = params[:api_token]
270           if !request.format.json? and request.method == 'GET'
271             # Repeat this request with api_token in the (new) session
272             # cookie instead of the query string.  This prevents API
273             # tokens from appearing in (and being inadvisedly copied
274             # and pasted from) browser Location bars.
275             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
276           else
277             yield
278           end
279         else
280           @errors = ['Invalid API token']
281           self.render_error status: 401
282         end
283       elsif session[:arvados_api_token]
284         # In this case, the token must have already verified at some
285         # point, but it might have been revoked since.  We'll try
286         # using it, and catch the exception if it doesn't work.
287         try_redirect_to_login = false
288         Thread.current[:arvados_api_token] = session[:arvados_api_token]
289         begin
290           yield
291         rescue ArvadosApiClient::NotLoggedInException
292           try_redirect_to_login = true
293         end
294       else
295         logger.debug "No token received, session is #{session.inspect}"
296       end
297       if try_redirect_to_login
298         unless login_optional
299           redirect_to_login
300         else
301           # login is optional for this route so go on to the regular controller
302           Thread.current[:arvados_api_token] = nil
303           yield
304         end
305       end
306     ensure
307       # Remove token in case this Thread is used for anything else.
308       Thread.current[:arvados_api_token] = nil
309     end
310   end
311
312   def thread_with_mandatory_api_token
313     thread_with_api_token do
314       yield
315     end
316   end
317
318   # This runs after thread_with_mandatory_api_token in the filter chain.
319   def thread_with_optional_api_token
320     if Thread.current[:arvados_api_token]
321       # We are already inside thread_with_mandatory_api_token.
322       yield
323     else
324       # We skipped thread_with_mandatory_api_token. Use the optional version.
325       thread_with_api_token(true) do
326         yield
327       end
328     end
329   end
330
331   def verify_api_token
332     begin
333       Link.where(uuid: 'just-verifying-my-api-token')
334       true
335     rescue ArvadosApiClient::NotLoggedInException
336       false
337     end
338   end
339
340   def ensure_current_user_is_admin
341     unless current_user and current_user.is_admin
342       @errors = ['Permission denied']
343       self.render_error status: 401
344     end
345   end
346
347   def check_user_agreements
348     if current_user && !current_user.is_active && current_user.is_invited
349       signatures = UserAgreement.signatures
350       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
351       @required_user_agreements = UserAgreement.all.map do |ua|
352         if not @signed_ua_uuids.index ua.uuid
353           Collection.find(ua.uuid)
354         end
355       end.compact
356       if @required_user_agreements.empty?
357         # No agreements to sign. Perhaps we just need to ask?
358         current_user.activate
359         if !current_user.is_active
360           logger.warn "#{current_user.uuid.inspect}: " +
361             "No user agreements to sign, but activate failed!"
362         end
363       end
364       if !current_user.is_active
365         render 'user_agreements/index'
366       end
367     end
368     true
369   end
370
371   def select_theme
372     return Rails.configuration.arvados_theme
373   end
374
375   @@notification_tests = []
376
377   @@notification_tests.push lambda { |controller, current_user|
378     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
379       return nil
380     end
381     return lambda { |view|
382       view.render partial: 'notifications/ssh_key_notification'
383     }
384   }
385
386   #@@notification_tests.push lambda { |controller, current_user|
387   #  Job.limit(1).where(created_by: current_user.uuid).each do
388   #    return nil
389   #  end
390   #  return lambda { |view|
391   #    view.render partial: 'notifications/jobs_notification'
392   #  }
393   #}
394
395   @@notification_tests.push lambda { |controller, current_user|
396     Collection.limit(1).where(created_by: current_user.uuid).each do
397       return nil
398     end
399     return lambda { |view|
400       view.render partial: 'notifications/collections_notification'
401     }
402   }
403
404   @@notification_tests.push lambda { |controller, current_user|
405     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
406       return nil
407     end
408     return lambda { |view|
409       view.render partial: 'notifications/pipelines_notification'
410     }
411   }
412
413   def check_user_notifications
414     @notification_count = 0
415     @notifications = []
416
417     if current_user
418       @showallalerts = false
419       @@notification_tests.each do |t|
420         a = t.call(self, current_user)
421         if a
422           @notification_count += 1
423           @notifications.push a
424         end
425       end
426     end
427
428     if @notification_count == 0
429       @notification_count = ''
430     end
431   end
432 end