Merge branch 'master' into 2290-user-activity
[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   around_filter :thread_clear
5   around_filter :thread_with_mandatory_api_token, :except => [:render_exception, :render_not_found]
6   around_filter :thread_with_optional_api_token
7   before_filter :find_object_by_uuid, :except => [:index, :render_exception, :render_not_found]
8   before_filter :check_user_agreements, :except => [:render_exception, :render_not_found]
9   before_filter :check_user_notifications, :except => [:render_exception, :render_not_found]
10   theme :select_theme
11
12   begin
13     rescue_from Exception,
14     :with => :render_exception
15     rescue_from ActiveRecord::RecordNotFound,
16     :with => :render_not_found
17     rescue_from ActionController::RoutingError,
18     :with => :render_not_found
19     rescue_from ActionController::UnknownController,
20     :with => :render_not_found
21     rescue_from ::AbstractController::ActionNotFound,
22     :with => :render_not_found
23   end
24
25   def unprocessable(message=nil)
26     @errors ||= []
27
28     @errors << message if message
29     render_error status: 422
30   end
31
32   def render_error(opts)
33     respond_to do |f|
34       # json must come before html here, so it gets used as the
35       # default format when js is requested by the client. This lets
36       # ajax:error callback parse the response correctly, even though
37       # the browser can't.
38       f.json { render opts.merge(json: {success: false, errors: @errors}) }
39       f.html { render opts.merge(controller: 'application', action: 'error') }
40     end
41   end
42
43   def render_exception(e)
44     logger.error e.inspect
45     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
46     if @object.andand.errors.andand.full_messages.andand.any?
47       @errors = @object.errors.full_messages
48     else
49       @errors = [e.to_s]
50     end
51     self.render_error status: 422
52   end
53
54   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
55     logger.error e.inspect
56     @errors = ["Path not found"]
57     self.render_error status: 404
58   end
59
60   def index
61     @objects ||= model_class.limit(200).all
62     respond_to do |f|
63       f.json { render json: @objects }
64       f.html { render }
65       f.js { render }
66     end
67   end
68
69   def show
70     if !@object
71       return render_not_found("object not found")
72     end
73     respond_to do |f|
74       f.json { render json: @object }
75       f.html {
76         if request.method == 'GET'
77           render
78         else
79           redirect_to params[:return_to] || @object
80         end
81       }
82       f.js { render }
83     end
84   end
85
86   def render_content
87     if !@object
88       return render_not_found("object not found")
89     end
90   end
91
92   def new
93     @object = model_class.new
94   end
95
96   def update
97     updates = params[@object.class.to_s.underscore.singularize.to_sym]
98     updates.keys.each do |attr|
99       if @object.send(attr).is_a? Hash and updates[attr].is_a? String
100         updates[attr] = Oj.load updates[attr]
101       end
102     end
103     if @object.update_attributes updates
104       show
105     else
106       self.render_error status: 422
107     end
108   end
109
110   def create
111     @object ||= model_class.new params[model_class.to_s.underscore.singularize]
112     @object.save!
113
114     respond_to do |f|
115       f.json { render json: @object }
116       f.html {
117         redirect_to(params[:return_to] || @object)
118       }
119       f.js { render }
120     end
121   end
122
123   def destroy
124     if @object.destroy
125       respond_to do |f|
126         f.json { render json: @object }
127         f.html {
128           redirect_to(params[:return_to] || :back)
129         }
130         f.js { render }
131       end
132     else
133       self.render_error status: 422
134     end
135   end
136
137   def current_user
138     if Thread.current[:arvados_api_token]
139       Thread.current[:user] ||= User.current
140     else
141       logger.error "No API token in Thread"
142       return nil
143     end
144   end
145
146   def model_class
147     controller_name.classify.constantize
148   end
149
150   def breadcrumb_page_name
151     (@breadcrumb_page_name ||
152      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
153      action_name)
154   end
155
156   def index_pane_list
157     %w(Recent)
158   end
159
160   def show_pane_list
161     %w(Attributes Metadata JSON API)
162   end
163
164   protected
165     
166   def find_object_by_uuid
167     if params[:id] and params[:id].match /\D/
168       params[:uuid] = params.delete :id
169     end
170     if params[:uuid].is_a? String
171       @object = model_class.find(params[:uuid])
172     else
173       @object = model_class.where(uuid: params[:uuid]).first
174     end
175   end
176
177   def thread_clear
178     Thread.current[:arvados_api_token] = nil
179     Thread.current[:user] = nil
180     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
181     yield
182     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
183   end
184
185   def thread_with_api_token(login_optional = false)
186     begin
187       try_redirect_to_login = true
188       if params[:api_token]
189         try_redirect_to_login = false
190         Thread.current[:arvados_api_token] = params[:api_token]
191         # Before copying the token into session[], do a simple API
192         # call to verify its authenticity.
193         if verify_api_token
194           session[:arvados_api_token] = params[:api_token]
195           if !request.format.json? and request.method == 'GET'
196             # Repeat this request with api_token in the (new) session
197             # cookie instead of the query string.  This prevents API
198             # tokens from appearing in (and being inadvisedly copied
199             # and pasted from) browser Location bars.
200             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
201           else
202             yield
203           end
204         else
205           @errors = ['Invalid API token']
206           self.render_error status: 401
207         end
208       elsif session[:arvados_api_token]
209         # In this case, the token must have already verified at some
210         # point, but it might have been revoked since.  We'll try
211         # using it, and catch the exception if it doesn't work.
212         try_redirect_to_login = false
213         Thread.current[:arvados_api_token] = session[:arvados_api_token]
214         begin
215           yield
216         rescue ArvadosApiClient::NotLoggedInException
217           try_redirect_to_login = true
218         end
219       else
220         logger.debug "No token received, session is #{session.inspect}"
221       end
222       if try_redirect_to_login
223         unless login_optional
224           respond_to do |f|
225             f.html {
226               if request.method == 'GET'
227                 redirect_to $arvados_api_client.arvados_login_url(return_to: request.url)
228               else
229                 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."
230                 redirect_to :back
231               end
232             }
233             f.json {
234               @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.']
235               self.render_error status: 422
236             }
237           end
238         else
239           # login is optional for this route so go on to the regular controller
240           Thread.current[:arvados_api_token] = nil
241           yield
242         end
243       end
244     ensure
245       # Remove token in case this Thread is used for anything else.
246       Thread.current[:arvados_api_token] = nil
247     end
248   end
249
250   def thread_with_mandatory_api_token
251     thread_with_api_token do
252       yield
253     end
254   end
255
256   # This runs after thread_with_mandatory_api_token in the filter chain.
257   def thread_with_optional_api_token
258     if Thread.current[:arvados_api_token]
259       # We are already inside thread_with_mandatory_api_token.
260       yield
261     else
262       # We skipped thread_with_mandatory_api_token. Use the optional version.
263       thread_with_api_token(true) do 
264         yield
265       end
266     end
267   end
268
269   def verify_api_token
270     begin
271       Link.where(uuid: 'just-verifying-my-api-token')
272       true
273     rescue ArvadosApiClient::NotLoggedInException
274       false
275     end
276   end
277
278   def ensure_current_user_is_admin
279     unless current_user and current_user.is_admin
280       @errors = ['Permission denied']
281       self.render_error status: 401
282     end
283   end
284
285   def check_user_agreements
286     if current_user && !current_user.is_active && current_user.is_invited
287       signatures = UserAgreement.signatures
288       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
289       @required_user_agreements = UserAgreement.all.map do |ua|
290         if not @signed_ua_uuids.index ua.uuid
291           Collection.find(ua.uuid)
292         end
293       end.compact
294       if @required_user_agreements.empty?
295         # No agreements to sign. Perhaps we just need to ask?
296         current_user.activate
297         if !current_user.is_active
298           logger.warn "#{current_user.uuid.inspect}: " +
299             "No user agreements to sign, but activate failed!"
300         end
301       end
302       if !current_user.is_active
303         render 'user_agreements/index'
304       end
305     end
306     true
307   end
308
309   def select_theme
310     return Rails.configuration.arvados_theme
311   end
312
313   @@notification_tests = []
314
315   @@notification_tests.push lambda { |controller, current_user|
316     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do   
317       return nil
318     end
319     return lambda { |view|
320       view.render partial: 'notifications/ssh_key_notification'
321     }
322   }
323
324   #@@notification_tests.push lambda { |controller, current_user|
325   #  Job.limit(1).where(created_by: current_user.uuid).each do
326   #    return nil
327   #  end
328   #  return lambda { |view|
329   #    view.render partial: 'notifications/jobs_notification'
330   #  }
331   #}
332
333   @@notification_tests.push lambda { |controller, current_user|
334     Collection.limit(1).where(created_by: current_user.uuid).each do
335       return nil
336     end
337     return lambda { |view|
338       view.render partial: 'notifications/collections_notification'
339     }
340   }
341
342   @@notification_tests.push lambda { |controller, current_user|
343     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
344       return nil
345     end
346     return lambda { |view|
347       view.render partial: 'notifications/pipelines_notification'
348     }
349   }
350
351   def check_user_notifications
352     @notification_count = 0
353     @notifications = []
354
355     if current_user
356       @showallalerts = false      
357       @@notification_tests.each do |t|
358         a = t.call(self, current_user)
359         if a
360           @notification_count += 1
361           @notifications.push a
362         end
363       end
364     end
365
366     if @notification_count == 0
367       @notification_count = ''
368     end
369   end
370 end