Merge remote-tracking branch 'origin/master' into pete-fixes
authorPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 24 Jun 2014 20:04:10 +0000 (16:04 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 24 Jun 2014 20:04:10 +0000 (16:04 -0400)
apps/workbench/app/assets/javascripts/log_viewer.js
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/sessions_controller.rb
apps/workbench/test/integration/errors_test.rb [new file with mode: 0644]
apps/workbench/test/integration/logins_test.rb
apps/workbench/test/integration_helper.rb
services/fuse/bin/arv-mount

index 101b4a5e807d6715a1264aa8bad89e61b9a86c12..74bba133fbc87fd99f34eaff0c2b4155cfa9e90e 100644 (file)
@@ -15,7 +15,7 @@ function addToLogViewer(logViewer, lines, taskState) {
         var v = lines[a].match(re);
         if (v != null) {
 
-            var ts = new Date(Date.UTC(v[2], v[3], v[4], v[6], v[7], v[8]));
+            var ts = new Date(Date.UTC(v[2], v[3]-1, v[4], v[6], v[7], v[8]));
 
             v11 = v[11];
             if (typeof v[11] === 'undefined') {
index ec2e95afd2ed065530459d25baf00a0f87d2ba6d..38ae911b5c3365e182362d9abc68824ef43aee15 100644 (file)
@@ -8,8 +8,10 @@ class ApplicationController < ActionController::Base
   ERROR_ACTIONS = [:render_error, :render_not_found]
 
   around_filter :thread_clear
-  around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS
-  around_filter :thread_with_optional_api_token
+  around_filter :set_thread_api_token
+  # Methods that don't require login should
+  #   skip_around_filter :require_thread_api_token
+  around_filter :require_thread_api_token, except: ERROR_ACTIONS
   before_filter :check_user_agreements, except: ERROR_ACTIONS
   before_filter :check_user_notifications, except: ERROR_ACTIONS
   before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS
@@ -53,13 +55,21 @@ class ApplicationController < ActionController::Base
     else
       @errors = [e.to_s]
     end
-    self.render_error status: 422
+    if e.is_a? ArvadosApiClient::NotLoggedInException
+      self.render_error status: 422
+    else
+      set_thread_api_token do
+        self.render_error status: 422
+      end
+    end
   end
 
   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
     logger.error e.inspect
     @errors = ["Path not found"]
-    self.render_error status: 404
+    set_thread_api_token do
+      self.render_error status: 404
+    end
   end
 
   def find_objects_for_index
@@ -294,11 +304,15 @@ class ApplicationController < ActionController::Base
 
   protected
 
+  def strip_token_from_path(path)
+    path.sub(/([\?&;])api_token=[^&;]*[&;]?/, '\1')
+  end
+
   def redirect_to_login
     respond_to do |f|
       f.html {
         if request.method.in? ['GET', 'HEAD']
-          redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
+          redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url))
         else
           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."
           redirect_to :back
@@ -357,58 +371,51 @@ class ApplicationController < ActionController::Base
     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
   end
 
-  def thread_with_api_token(login_optional = false)
+  # Save the session API token in thread-local storage, and yield.
+  # This method also takes care of session setup if the request
+  # provides a valid api_token parameter.
+  # If a token is unavailable or expired, the block is still run, with
+  # a nil token.
+  def set_thread_api_token
+    # If an API token has already been found, pass it through.
+    if Thread.current[:arvados_api_token]
+      yield
+      return
+    end
+
     begin
-      try_redirect_to_login = true
-      if params[:api_token]
-        try_redirect_to_login = false
-        Thread.current[:arvados_api_token] = params[:api_token]
-        # Before copying the token into session[], do a simple API
-        # call to verify its authenticity.
-        if verify_api_token
-          session[:arvados_api_token] = params[:api_token]
-          u = User.current
-          session[:user] = {
-            uuid: u.uuid,
-            email: u.email,
-            first_name: u.first_name,
-            last_name: u.last_name,
-            is_active: u.is_active,
-            is_admin: u.is_admin,
-            prefs: u.prefs
-          }
-          if !request.format.json? and request.method.in? ['GET', 'HEAD']
-            # Repeat this request with api_token in the (new) session
-            # cookie instead of the query string.  This prevents API
-            # tokens from appearing in (and being inadvisedly copied
-            # and pasted from) browser Location bars.
-            redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
-          else
-            yield
-          end
-        else
-          @errors = ['Invalid API token']
-          self.render_error status: 401
-        end
-      elsif session[:arvados_api_token]
-        # In this case, the token must have already verified at some
-        # point, but it might have been revoked since.  We'll try
-        # using it, and catch the exception if it doesn't work.
-        try_redirect_to_login = false
-        Thread.current[:arvados_api_token] = session[:arvados_api_token]
-        begin
-          yield
-        rescue ArvadosApiClient::NotLoggedInException
-          try_redirect_to_login = true
+      # If there's a valid api_token parameter, use it to set up the session.
+      if (Thread.current[:arvados_api_token] = params[:api_token]) and
+          verify_api_token
+        session[:arvados_api_token] = params[:api_token]
+        u = User.current
+        session[:user] = {
+          uuid: u.uuid,
+          email: u.email,
+          first_name: u.first_name,
+          last_name: u.last_name,
+          is_active: u.is_active,
+          is_admin: u.is_admin,
+          prefs: u.prefs
+        }
+        if !request.format.json? and request.method.in? ['GET', 'HEAD']
+          # Repeat this request with api_token in the (new) session
+          # cookie instead of the query string.  This prevents API
+          # tokens from appearing in (and being inadvisedly copied
+          # and pasted from) browser Location bars.
+          redirect_to strip_token_from_path(request.fullpath)
+          return
         end
-      else
-        logger.debug "No token received, session is #{session.inspect}"
       end
-      if try_redirect_to_login
-        unless login_optional
-          redirect_to_login
-        else
-          # login is optional for this route so go on to the regular controller
+
+      # With setup done, handle the request using the session token.
+      Thread.current[:arvados_api_token] = session[:arvados_api_token]
+      begin
+        yield
+      rescue ArvadosApiClient::NotLoggedInException
+        # If we got this error with a token, it must've expired.
+        # Retry the request without a token.
+        unless Thread.current[:arvados_api_token].nil?
           Thread.current[:arvados_api_token] = nil
           yield
         end
@@ -419,32 +426,18 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def thread_with_mandatory_api_token
-    thread_with_api_token(true) do
-      if Thread.current[:arvados_api_token]
-        yield
-      elsif session[:arvados_api_token]
-        # Expired session. Clear it before refreshing login so that,
-        # if this login procedure fails, we end up showing the "please
-        # log in" page instead of getting stuck in a redirect loop.
-        session.delete :arvados_api_token
-        redirect_to_login
-      else
-        render 'users/welcome'
-      end
-    end
-  end
-
-  # This runs after thread_with_mandatory_api_token in the filter chain.
-  def thread_with_optional_api_token
+  # Reroute this request if an API token is unavailable.
+  def require_thread_api_token
     if Thread.current[:arvados_api_token]
-      # We are already inside thread_with_mandatory_api_token.
       yield
+    elsif session[:arvados_api_token]
+      # Expired session. Clear it before refreshing login so that,
+      # if this login procedure fails, we end up showing the "please
+      # log in" page instead of getting stuck in a redirect loop.
+      session.delete :arvados_api_token
+      redirect_to_login
     else
-      # We skipped thread_with_mandatory_api_token. Use the optional version.
-      thread_with_api_token(true) do
-        yield
-      end
+      render 'users/welcome'
     end
   end
 
index 9179848a4d657baeb1d65a465ac21f479b874bd5..95aee92e1959c6828b30ad0fcbeb5525a391e4db 100644 (file)
@@ -1,5 +1,5 @@
 class CollectionsController < ApplicationController
-  skip_around_filter(:thread_with_mandatory_api_token,
+  skip_around_filter(:require_thread_api_token,
                      only: [:show_file, :show_file_links])
   skip_before_filter(:find_object_by_uuid,
                      only: [:provenance, :show_file, :show_file_links])
index 591373b6c2c6526a85f26d5211ca8a94f1468ab0..97c8d5a9dd4dc7f81184d1a62d7222110a169770 100644 (file)
@@ -1,6 +1,6 @@
 class SessionsController < ApplicationController
-  skip_around_filter :thread_with_mandatory_api_token, :only => [:destroy, :index]
-  skip_around_filter :thread_with_optional_api_token, :only => [:destroy, :index]
+  skip_around_filter :require_thread_api_token, :only => [:destroy, :index]
+  skip_around_filter :set_thread_api_token, :only => [:destroy, :index]
   skip_before_filter :find_object_by_uuid, :only => [:destroy, :index]
 
   def destroy
diff --git a/apps/workbench/test/integration/errors_test.rb b/apps/workbench/test/integration/errors_test.rb
new file mode 100644 (file)
index 0000000..092041d
--- /dev/null
@@ -0,0 +1,19 @@
+require 'integration_helper'
+
+class ErrorsTest < ActionDispatch::IntegrationTest
+  BAD_UUID = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+
+  test "error page renders user navigation" do
+    visit(page_with_token("active", "/collections/#{BAD_UUID}"))
+    assert(page.has_text?(@@API_AUTHS["active"]["email"]),
+           "User information missing from error page")
+    assert(page.has_no_text?(/log ?in/i),
+           "Logged in user prompted to log in on error page")
+  end
+
+  test "error page renders without login" do
+    visit "/collections/download/#{BAD_UUID}/#{@@API_AUTHS['active']['api_token']}"
+    assert(page.has_no_text?(/\b500\b/),
+           "Error page without login returned 500")
+  end
+end
index be7e4e15a5692d5b47ce433fb5647cbb7159bd29..9daf831810de1253496f6c91ee6a931525b5c765 100644 (file)
@@ -7,15 +7,12 @@ class LoginsTest < ActionDispatch::IntegrationTest
     assert_no_match(/\bapi_token=/, current_path)
   end
 
-  test "can't use expired token" do
+  test "trying to use expired token redirects to login page" do
     visit page_with_token('expired_trustedclient')
-    assert page.has_text? 'Log in'
-  end
-
-  test "expired token yields login page, not error page" do
-    visit page_with_token('expired_trustedclient')
-    # Even the error page has a "Log in" link. We should look for
-    # something that only appears the real login page.
-    assert page.has_text? ' Log in Oh... fiddlesticks. Sorry, I had some trouble handling your request'
+    buttons = all("a.btn", text: /Log in/)
+    assert_equal(1, buttons.size, "Failed to find one login button")
+    login_link = buttons.first[:href]
+    assert_match(%r{//[^/]+/login}, login_link)
+    assert_no_match(/\bapi_token=/, login_link)
   end
 end
index ebfbc584e8fe0878ac01a86920534cb38074e938..81ea67c3c5c4bd8925e650501a85a258d6dff855 100644 (file)
@@ -54,11 +54,17 @@ class ActionDispatch::IntegrationTest
     end
   end
 
-  @@screenshot_count = 0
+  @@screenshot_count = 1
   def screenshot
-    image_file = "./tmp/workbench-fail-#{@@screenshot_count += 1}.png"
-    page.save_screenshot image_file
-    puts "Saved #{image_file}"
+    image_file = "./tmp/workbench-fail-#{@@screenshot_count}.png"
+    begin
+      page.save_screenshot image_file
+    rescue Capybara::NotSupportedByDriverError
+      # C'est la vie.
+    else
+      puts "Saved #{image_file}"
+      @@screenshot_count += 1
+    end
   end
 
   teardown do
index 2988c25c7d96895f20fe843c39ce45394c90bdd0..726741e3b01619b79b62094662d76e819e9b38df 100755 (executable)
@@ -1,11 +1,13 @@
 #!/usr/bin/env python
 
-from arvados_fuse import *
-import arvados
-import subprocess
 import argparse
+import arvados
 import daemon
+import os
 import signal
+import subprocess
+
+from arvados_fuse import *
 
 if __name__ == '__main__':
     # Handle command line parameters
@@ -94,12 +96,9 @@ collections on the server.""")
 
         exit(rc)
     else:
-        if args.foreground:
-            # Initialize the fuse connection
-            llfuse.init(operations, args.mountpoint, opts)
-            llfuse.main()
-        else:
-            # Initialize the fuse connection
-            llfuse.init(operations, args.mountpoint, opts)
-            with daemon.DaemonContext():
-                llfuse.main()
+        os.chdir(args.mountpoint)
+        if not args.foreground:
+            daemon_ctx = daemon.DaemonContext(working_directory='.')
+            daemon_ctx.open()
+        llfuse.init(operations, '.', opts)
+        llfuse.main()