Merge branch 'master' into 3118-docker-fixes
authorWard Vandewege <ward@curoverse.com>
Tue, 1 Jul 2014 20:38:47 +0000 (16:38 -0400)
committerWard Vandewege <ward@curoverse.com>
Tue, 1 Jul 2014 20:38:47 +0000 (16:38 -0400)
52 files changed:
apps/workbench/app/assets/images/spinner_32px.gif [moved from apps/workbench/app/assets/images/ajax-loader.gif with 100% similarity]
apps/workbench/app/assets/javascripts/infinite_scroll.js
apps/workbench/app/assets/javascripts/select_modal.js
apps/workbench/app/assets/stylesheets/loading.css.scss.erb [moved from apps/workbench/app/assets/stylesheets/loading.css with 78% similarity]
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/arvados_api_client.rb
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/views/application/404.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/404.json.erb [new file with mode: 0644]
apps/workbench/app/views/application/_choose.js.erb
apps/workbench/app/views/application/_content.html.erb
apps/workbench/app/views/application/api_error.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/api_error.json.erb [new file with mode: 0644]
apps/workbench/app/views/collections/_index_tbody.html.erb
apps/workbench/app/views/collections/_show_recent.html.erb
apps/workbench/app/views/jobs/_show_log.html.erb
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/pipeline_instances/_show_recent.html.erb
apps/workbench/app/views/pipeline_templates/_show_components.html.erb
apps/workbench/app/views/pipeline_templates/_show_recent.html.erb
apps/workbench/app/views/users/_tables.html.erb
apps/workbench/test/functional/application_controller_test.rb
apps/workbench/test/functional/collections_controller_test.rb
apps/workbench/test/functional/users_controller_test.rb
apps/workbench/test/integration/errors_test.rb
apps/workbench/test/integration/pipeline_instances_test.rb
docker/build_tools/Makefile
docker/bwa-samtools/Dockerfile [new file with mode: 0644]
sdk/cli/bin/arv
sdk/cli/bin/crunch-job
services/api/Gemfile
services/api/Gemfile.lock
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
services/api/app/controllers/arvados/v1/jobs_controller.rb
services/api/app/controllers/arvados/v1/links_controller.rb
services/api/app/models/user.rb
services/api/db/migrate/20140627210837_anonymous_group.rb [new file with mode: 0644]
services/api/db/schema.rb
services/api/db/seeds.rb
services/api/lib/current_api_client.rb
services/api/script/crunch-dispatch.rb
services/api/script/get_anonymous_user_token.rb [new file with mode: 0755]
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/groups.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/users.yml
services/api/test/functional/application_controller_test.rb [new file with mode: 0644]
services/api/test/unit/user_test.rb
services/fuse/bin/arv-mount

index 00db2ba7e320b1ab8b371e2eae698a99f946988a..a17b446c465280127a476b108587f39bcbc10b87 100644 (file)
@@ -3,6 +3,7 @@ function maybe_load_more_content() {
     var container;              // element that receives new content
     var src;                    // url for retrieving content
     var scrollHeight;
+    var spinner, colspan;
     scrollHeight = scroller.scrollHeight || $('body')[0].scrollHeight;
     if ($(scroller).scrollTop() + $(scroller).height()
         >
@@ -14,12 +15,27 @@ function maybe_load_more_content() {
             return;
         // Don't start another request until this one finishes
         $(container).attr('data-infinite-content-href', null);
-        $(container).append('<img src="/assets/ajax-loader.gif" class="infinite-scroller-spinner"></img>');
+        spinner = '<div class="spinner spinner-32px spinner-h-center"></div>';
+        if ($(container).is('table,tbody,thead,tfoot')) {
+            // Hack to determine how many columns a new tr should have
+            // in order to reach full width.
+            colspan = $(container).closest('table').
+                find('tr').eq(0).find('td,th').length;
+            if (colspan == 0)
+                colspan = '*';
+            spinner = ('<tr class="spinner"><td colspan="' + colspan + '">' +
+                       spinner +
+                       '</td></tr>');
+        }
+        $(container).append(spinner);
         $.ajax(src,
                {dataType: 'json',
                 type: 'GET',
                 data: {},
                 context: {container: container, src: src}}).
+            always(function() {
+                $(this.container).find(".spinner").detach();
+            }).
             fail(function(jqxhr, status, error) {
                 if (jqxhr.readyState == 0 || jqxhr.status == 0) {
                     message = "Cancelled."
@@ -33,7 +49,6 @@ function maybe_load_more_content() {
                 $(this.container).attr('data-infinite-content-href', this.src);
             }).
             done(function(data, status, jqxhr) {
-                $(this.container).find(".infinite-scroller-spinner").detach();
                 $(this.container).append(data.content);
                 $(this.container).attr('data-infinite-content-href', data.next_page_href);
             });
@@ -44,7 +59,7 @@ $(document).
         $('[data-infinite-scroller]').each(function() {
             var $scroller = $($(this).attr('data-infinite-scroller'));
             if (!$scroller.hasClass('smart-scroll') &&
-               'scroll' != $scroller.css('overflow-y'))
+                'scroll' != $scroller.css('overflow-y'))
                 $scroller = $(window);
             $scroller.
                 addClass('infinite-scroller').
index 9107a010a87ea4f83b5229baa14fe396b50ac75a..0a58213eb9ffdea8d16efca470d59e1cc796a294 100644 (file)
@@ -16,7 +16,7 @@ $(document).on('click', '.selectable', function() {
         prop('disabled', !any);
 
     if ($this.hasClass('active')) {
-        $(".modal-dialog-preview-pane").html('<img src="/assets/ajax-loader.gif"></img>');
+        $(".modal-dialog-preview-pane").html('<div class="spinner spinner-32px spinner-h-center spinner-v-center"></div>');
         $.ajax($this.attr('data-preview-href'),
                {dataType: "html"}).
            done(function(data, status, jqxhr) {
similarity index 78%
rename from apps/workbench/app/assets/stylesheets/loading.css
rename to apps/workbench/app/assets/stylesheets/loading.css.scss.erb
index 640f702510f6abfc9403955cc0082d0b5948a606..9f74866f53539c8e919402f42146c88e5169129a 100644 (file)
@@ -2,6 +2,27 @@
     opacity: 0;
 }
 
+.spinner {
+    /* placeholder for stuff like $.find('.spinner').detach() */
+}
+
+.spinner-32px {
+    background-image: url('<%= asset_path('spinner_32px.gif') %>');
+    background-repeat: no-repeat;
+    width: 32px;
+    height: 32px;
+}
+
+.spinner-h-center {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.spinner-v-center {
+    position: relative;
+    top: 45%;
+}
+
 .rotating {
     color: #f00;
     /* Chrome and Firefox, at least in Linux, render a horrible shaky
index e3f92efe237dd9668add6813bb5d228b79b65bb8..7d7ea9534b381b74cf27d7ac6bd15b9ce436c1fc 100644 (file)
@@ -35,40 +35,54 @@ class ApplicationController < ActionController::Base
     render_error status: 422
   end
 
-  def render_error(opts)
-    opts = {status: 500}.merge opts
+  def render_error(opts={})
+    opts[:status] ||= 500
     respond_to do |f|
       # json must come before html here, so it gets used as the
       # default format when js is requested by the client. This lets
       # ajax:error callback parse the response correctly, even though
       # the browser can't.
       f.json { render opts.merge(json: {success: false, errors: @errors}) }
-      f.html { render opts.merge(controller: 'application', action: 'error') }
+      f.html { render({action: 'error'}.merge(opts)) }
     end
   end
 
   def render_exception(e)
     logger.error e.inspect
     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
-    if @object.andand.errors.andand.full_messages.andand.any?
+    err_opts = {status: 422}
+    if e.is_a?(ArvadosApiClient::ApiError)
+      err_opts.merge!(action: 'api_error', locals: {api_error: e})
+      @errors = e.api_response[:errors]
+    elsif @object.andand.errors.andand.full_messages.andand.any?
       @errors = @object.errors.full_messages
     else
       @errors = [e.to_s]
     end
-    if e.is_a? ArvadosApiClient::NotLoggedInException
-      self.render_error status: 422
-    else
-      set_thread_api_token do
-        self.render_error status: 422
-      end
+    # If the user has an active session, and the API server is available,
+    # make user information available on the error page.
+    begin
+      load_api_token(session[:arvados_api_token])
+    rescue ArvadosApiClient::ApiError
+      load_api_token(nil)
+    end
+    # Preload projects trees for the template.  If that fails, set empty
+    # trees so error page rendering can proceed.  (It's easier to rescue the
+    # exception here than in a template.)
+    begin
+      build_project_trees
+    rescue ArvadosApiClient::ApiError
+      @my_project_tree ||= []
+      @shared_project_tree ||= []
     end
+    render_error(err_opts)
   end
 
   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
     logger.error e.inspect
     @errors = ["Path not found"]
     set_thread_api_token do
-      self.render_error status: 404
+      self.render_error(action: '404', status: 404)
     end
   end
 
@@ -266,22 +280,7 @@ class ApplicationController < ActionController::Base
   end
 
   def current_user
-    return Thread.current[:user] if Thread.current[:user]
-
-    if Thread.current[:arvados_api_token]
-      if session[:user]
-        if session[:user][:is_active] != true
-          Thread.current[:user] = User.current
-        else
-          Thread.current[:user] = User.new(session[:user])
-        end
-      else
-        Thread.current[:user] = User.current
-      end
-    else
-      logger.error "No API token in Thread"
-      return nil
-    end
+    Thread.current[:user]
   end
 
   def model_class
@@ -331,8 +330,7 @@ class ApplicationController < ActionController::Base
     [:arvados_api_token, :user].each do |key|
       start_values[key] = Thread.current[key]
     end
-    Thread.current[:arvados_api_token] = api_token
-    Thread.current[:user] = nil
+    load_api_token(api_token)
     begin
       yield
     ensure
@@ -344,85 +342,111 @@ class ApplicationController < ActionController::Base
     if params[:id] and params[:id].match /\D/
       params[:uuid] = params.delete :id
     end
-    if not model_class
-      @object = nil
-    elsif params[:uuid].is_a? String
-      if params[:uuid].empty?
+    begin
+      if not model_class
         @object = nil
+      elsif not params[:uuid].is_a?(String)
+        @object = model_class.where(uuid: params[:uuid]).first
+      elsif params[:uuid].empty?
+        @object = nil
+      elsif (model_class != Link and
+             resource_class_for_uuid(params[:uuid]) == Link)
+        @name_link = Link.find(params[:uuid])
+        @object = model_class.find(@name_link.head_uuid)
       else
-        if (model_class != Link and
-            resource_class_for_uuid(params[:uuid]) == Link)
-          @name_link = Link.find(params[:uuid])
-          @object = model_class.find(@name_link.head_uuid)
-        else
-          @object = model_class.find(params[:uuid])
-        end
+        @object = model_class.find(params[:uuid])
       end
-    else
-      @object = model_class.where(uuid: params[:uuid]).first
+    rescue ArvadosApiClient::NotFoundException, RuntimeError => error
+      if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/)
+        raise
+      end
+      render_not_found(error)
+      return false
     end
   end
 
   def thread_clear
-    Thread.current[:arvados_api_token] = nil
-    Thread.current[:user] = nil
+    load_api_token(nil)
     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
     yield
     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
   end
 
+  # Set up the thread with the given API token and associated user object.
+  def load_api_token(new_token)
+    Thread.current[:arvados_api_token] = new_token
+    if new_token.nil?
+      Thread.current[:user] = nil
+    elsif (new_token == session[:arvados_api_token]) and
+        session[:user].andand[:is_active]
+      Thread.current[:user] = User.new(session[:user])
+    else
+      Thread.current[:user] = User.current
+    end
+  end
+
+  # If there's a valid api_token parameter, set up the session with that
+  # user's information.  Return true if the method redirects the request
+  # (usually a post-login redirect); false otherwise.
+  def setup_user_session
+    return false unless params[:api_token]
+    Thread.current[:arvados_api_token] = params[:api_token]
+    begin
+      user = User.current
+    rescue ArvadosApiClient::NotLoggedInException
+      false  # We may redirect to login, or not, based on the current action.
+    else
+      session[:arvados_api_token] = params[:api_token]
+      session[:user] = {
+        uuid: user.uuid,
+        email: user.email,
+        first_name: user.first_name,
+        last_name: user.last_name,
+        is_active: user.is_active,
+        is_admin: user.is_admin,
+        prefs: user.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)
+        true
+      else
+        false
+      end
+    ensure
+      Thread.current[:arvados_api_token] = nil
+    end
+  end
+
   # 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
+      yield   # An API token has already been found - pass it through.
       return
+    elsif setup_user_session
+      return  # A new session was set up and received a response.
     end
 
     begin
-      # 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
-      end
-
-      # With setup done, handle the request using the session token.
-      Thread.current[:arvados_api_token] = session[:arvados_api_token]
-      begin
+      load_api_token(session[:arvados_api_token])
+      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?
+        load_api_token(nil)
         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
       end
     ensure
       # Remove token in case this Thread is used for anything else.
-      Thread.current[:arvados_api_token] = nil
+      load_api_token(nil)
     end
   end
 
@@ -441,15 +465,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def verify_api_token
-    begin
-      Link.where(uuid: 'just-verifying-my-api-token')
-      true
-    rescue ArvadosApiClient::NotLoggedInException
-      false
-    end
-  end
-
   def ensure_current_user_is_admin
     unless current_user and current_user.is_admin
       @errors = ['Permission denied']
index 95aee92e1959c6828b30ad0fcbeb5525a391e4db..5a7a52207bcebdc946eb3289c618d97b2cddd78c 100644 (file)
@@ -118,7 +118,7 @@ class CollectionsController < ApplicationController
 
   def show_file_links
     Thread.current[:reader_tokens] = [params[:reader_token]]
-    find_object_by_uuid
+    return if false.equal?(find_object_by_uuid)
     render layout: false
   end
 
@@ -238,18 +238,14 @@ class CollectionsController < ApplicationController
     # error we encounter, and return nil.
     most_specific_error = [401]
     token_list.each do |api_token|
-      using_specific_api_token(api_token) do
-        begin
+      begin
+        using_specific_api_token(api_token) do
           yield
           return api_token
-        rescue ArvadosApiClient::NotLoggedInException => error
-          status = 401
-        rescue => error
-          status = (error.message =~ /\[API: (\d+)\]$/) ? $1.to_i : nil
-          raise unless [401, 403, 404].include?(status)
         end
-        if status >= most_specific_error.first
-          most_specific_error = [status, error]
+      rescue ArvadosApiClient::ApiError => error
+        if error.api_status >= most_specific_error.first
+          most_specific_error = [error.api_status, error]
         end
       end
     end
index 80eb16a1cedeb71d211741a020ef69c8b1e20e89..d1bac0c4cd2d1c8e24d60ef4c34327079643fd8b 100644 (file)
@@ -100,7 +100,7 @@ module ApplicationHelper
               else
                 link_name = object_for_dataclass(resource_class, link_uuid).andand.friendly_link_name
               end
-            rescue RuntimeError
+            rescue ArvadosApiClient::NotFoundException
               # If that lookup failed, the link will too. So don't make one.
               return attrvalue
             end
index 25c54d1e9ce76385488aff128074c1bdc47a146e..9f34a29e5bfffb2c5df33299cb9b3b0181509681 100644 (file)
@@ -2,13 +2,57 @@ require 'httpclient'
 require 'thread'
 
 class ArvadosApiClient
-  class NotLoggedInException < StandardError
+  class ApiError < StandardError
+    attr_reader :api_response, :api_response_s, :api_status, :request_url
+
+    def initialize(request_url, errmsg)
+      @request_url = request_url
+      @api_response ||= {}
+      super(errmsg)
+    end
   end
-  class InvalidApiResponseException < StandardError
+
+  class NoApiResponseException < ApiError
+    def initialize(request_url, exception)
+      @api_response_s = exception.to_s
+      super(request_url,
+            "#{exception.class.to_s} error connecting to API server")
+    end
   end
-  class AccessForbiddenException < StandardError
+
+  class InvalidApiResponseException < ApiError
+    def initialize(request_url, api_response)
+      @api_status = api_response.status_code
+      @api_response_s = api_response.content
+      super(request_url, "Unparseable response from API server")
+    end
+  end
+
+  class ApiErrorResponseException < ApiError
+    def initialize(request_url, api_response)
+      @api_status = api_response.status_code
+      @api_response_s = api_response.content
+      @api_response = Oj.load(@api_response_s, :symbol_keys => true)
+      errors = @api_response[:errors]
+      if errors.respond_to?(:join)
+        errors = errors.join("\n\n")
+      else
+        errors = errors.to_s
+      end
+      super(request_url, "#{errors} [API: #{@api_status}]")
+    end
   end
 
+  class AccessForbiddenException < ApiErrorResponseException; end
+  class NotFoundException < ApiErrorResponseException; end
+  class NotLoggedInException < ApiErrorResponseException; end
+
+  ERROR_CODE_CLASSES = {
+    401 => NotLoggedInException,
+    403 => AccessForbiddenException,
+    404 => NotFoundException,
+  }
+
   @@profiling_enabled = Rails.configuration.profiling_enabled
   @@discovery = nil
 
@@ -78,35 +122,26 @@ class ArvadosApiClient
 
     profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]} #{query[:filters]}" }
     msg = @client_mtx.synchronize do
-      @api_client.post(url,
-                       query,
-                       header: header)
+      begin
+        @api_client.post(url, query, header: header)
+      rescue => exception
+        raise NoApiResponseException.new(url, exception)
+      end
     end
     profile_checkpoint 'API transaction'
 
-    if msg.status_code == 401
-      raise NotLoggedInException.new
-    end
-
-    json = msg.content
-
     begin
-      resp = Oj.load(json, :symbol_keys => true)
+      resp = Oj.load(msg.content, :symbol_keys => true)
     rescue Oj::ParseError
-      raise InvalidApiResponseException.new json
+      resp = nil
     end
     if not resp.is_a? Hash
-      raise InvalidApiResponseException.new json
-    end
-    if msg.status_code != 200
-      errors = resp[:errors]
-      errors = errors.join("\n\n") if errors.is_a? Array
-      if msg.status_code == 403
-        raise AccessForbiddenException.new "#{errors} [API: #{msg.status_code}]"
-      else
-        raise "#{errors} [API: #{msg.status_code}]"
-      end
+      raise InvalidApiResponseException.new(url, msg)
+    elsif msg.status_code != 200
+      error_class = ERROR_CODE_CLASSES.fetch(msg.status_code, ApiError)
+      raise error_class.new(url, msg)
     end
+
     if resp[:_profile]
       Rails.logger.info "API client: " \
       "#{resp.delete(:_profile)[:request_time]} request_time"
index 3b5ac86f91c0c0f0134045a8e75a19bbc76c536d..6d427fdda1114658f09b3ed054e1856636a5cc1e 100644 (file)
@@ -56,7 +56,7 @@ class ArvadosBase < ActiveRecord::Base
   end
 
   def self.columns
-    return @columns unless @columns.nil?
+    return @columns if @columns.andand.any?
     @columns = []
     @attribute_info ||= {}
     schema = arvados_api_client.discovery[:schemas][self.to_s.to_sym]
diff --git a/apps/workbench/app/views/application/404.html.erb b/apps/workbench/app/views/application/404.html.erb
new file mode 100644 (file)
index 0000000..40d73b9
--- /dev/null
@@ -0,0 +1,21 @@
+<%
+   if (controller.andand.action_name == 'show') and params[:uuid]
+     class_name = controller.model_class.to_s.underscore.humanize(capitalize: false)
+     req_item = safe_join([class_name, " with UUID ",
+                           raw("<code>"), params[:uuid], raw("</code>")], "")
+   else
+     req_item = "page you requested"
+   end
+%>
+
+<h2>Not Found</h2>
+
+<p>The <%= req_item %> was not found.
+
+<% if class_name %>
+Perhaps you'd like to
+<%= link_to("browse all #{class_name.pluralize}", action: :index) %>?
+<% end %>
+
+</p>
+
diff --git a/apps/workbench/app/views/application/404.json.erb b/apps/workbench/app/views/application/404.json.erb
new file mode 100644 (file)
index 0000000..8371ff9
--- /dev/null
@@ -0,0 +1 @@
+{"errors":<%= raw @errors.to_json %>}
\ No newline at end of file
index 6521b0ccf112c14ab8049a1dc5b8b20c8e46ae5b..b033c9bf2fedf170c765b178bb510494707888ca 100644 (file)
@@ -6,7 +6,7 @@ $('body > .modal-container .modal .modal-footer .btn-primary').
     attr('data-method', '<%= j params[:action_method] %>').
     data('action-data', <%= raw params[:action_data] %>);
 $(".chooser-show-project").on("click", function() {
-  $("#choose-scroll").html("<%=j image_tag 'ajax-loader.gif' %>");
+  $("#choose-scroll").html("<div class=\"spinner spinner-32px spinner-h-center\"></div>");
   $(".modal-dialog-preview-pane").html('');
   var t = $(this);
   var d = {
index 02f2e6f88fa729b6e89066201f9a2f9eff9f02ab..c9522de3f2548e81f0544d8afb9088f4afd03640 100644 (file)
@@ -85,7 +85,7 @@
           <%= render(partial: 'show_' + pane.downcase,
                      locals: { comparable: comparable, objects: @objects }) %>
           <% else %>
-            <%= image_tag 'ajax-loader.gif' %>
+            <div class="spinner spinner-32px spinner-h-center"></div>
         <% end %>
       </div>
     </div>
diff --git a/apps/workbench/app/views/application/api_error.html.erb b/apps/workbench/app/views/application/api_error.html.erb
new file mode 100644 (file)
index 0000000..10ffbb7
--- /dev/null
@@ -0,0 +1,23 @@
+<h2>Oh... fiddlesticks.</h2>
+
+<p>An error occurred when Workbench sent a request to the Arvados API server.  Try reloading this page.  If the problem is temporary, your request might go through next time.
+
+<% if not api_error %>
+</p>
+<% else %>
+If that doesn't work, the information below can help system administrators track down the problem.
+</p>
+
+<dl>
+  <dt>API request URL</dt>
+  <dd><code><%= api_error.request_url %></code></dd>
+
+  <% if api_error.api_response.empty? %>
+  <dt>Invalid API response</dt>
+  <dd><%= api_error.api_response_s %></dd>
+  <% else %>
+  <dt>API response</dt>
+  <dd><pre><%= Oj.dump(api_error.api_response, indent: 2) %></pre></dd>
+  <% end %>
+</dl>
+<% end %>
diff --git a/apps/workbench/app/views/application/api_error.json.erb b/apps/workbench/app/views/application/api_error.json.erb
new file mode 100644 (file)
index 0000000..8371ff9
--- /dev/null
@@ -0,0 +1 @@
+{"errors":<%= raw @errors.to_json %>}
\ No newline at end of file
index deba78e47b7f2723b2fa10da36a7674b357a123f..ec5a09e3c6a8a1c676cabb74d0f9fc40e75c2770 100644 (file)
@@ -26,7 +26,7 @@
     <% end %>
   </td>
   <td>
-    <%= raw(distance_of_time_in_words(c.created_at, Time.now).sub('about ','~').sub(' ','&nbsp;')) if c.created_at %>
+    <%= c.created_at.to_s if c.created_at %>
   </td>
   <td>
     <% current_state = @collection_info[c.uuid][:wanted_by_me] ? 'persistent' : 'cache' %>
index 80640ad3991bb20ada08bc5154a4ac2806f5f685..21a815c40cc0b0cb7363263ae43c76fa36dcbed8 100644 (file)
@@ -33,7 +33,7 @@
       <th></th>
       <th>uuid</th>
       <th>contents</th>
-      <th>age</th>
+      <th>created at</th>
       <th>storage</th>
       <th>tags</th>
     </tr>
index 743045c0a1371542cf5b8e558637e6983d190597..f1466aab7d8ae8814966fd51323c6ebfab809548 100644 (file)
@@ -43,16 +43,16 @@ var makeFilter = function() {
   <% logcollection = Collection.find @object.log %>
   <% if logcollection %>
     $.ajax('<%=j url_for logcollection %>/<%=j logcollection.files[0][1] %>').
-    done(function(data, status, jqxhr) {
-    logViewer.filter();
-    addToLogViewer(logViewer, data.split("\n"), taskState);
-    logViewer.filter(makeFilter());
-    generateJobOverview("#log-viewer-overview", logViewer, taskState);
-    $("#logloadspinner").detach();
-    }).
-    fail(function(jqxhr, status, error) {
-    $("#logloadspinner").detach();
-    });
+       done(function(data, status, jqxhr) {
+           logViewer.filter();
+           addToLogViewer(logViewer, data.split("\n"), taskState);
+           logViewer.filter(makeFilter());
+           generateJobOverview("#log-viewer-overview", logViewer, taskState);
+           $("#log-viewer .spinner").detach();
+       }).
+       fail(function(jqxhr, status, error) {
+           $("#log-viewer .spinner").detach();
+       });
   <% end %>
 <% else %>
   <%# Live log loading not implemented yet. %>
@@ -204,7 +204,7 @@ $("#set-show-failed-only").on("click", function() {
     </table>
 
     <% if @object.log and logcollection %>
-      <%= image_tag 'ajax-loader.gif', id: "logloadspinner" %>
+      <div class="spinner spinner-32px"></div>
     <% end %>
 
   </div>
index 23e071525f254db74e4af36479aa9478ee40b5b3..63de6c267e1b308dd8ffc5a8cd233ced20649169 100644 (file)
@@ -74,7 +74,6 @@
           </li>
           -->
 
-          <% if current_user %>
           <li class="dropdown notification-menu">
             <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
               <span class="badge badge-alert notification-count"><%= @notification_count %></span>
@@ -97,7 +96,6 @@
               <% end %>
             </ul>
           </li>
-          <% end %>
 
           <li class="dropdown selection-menu">
             <a href="#" class="dropdown-toggle" data-toggle="dropdown">
               </a>
               <ul class="dropdown-menu" role="menu">
                 <li role="presentation" class="dropdown-header">
-                  System tools
+                  Settings
                 </li>
                 <li role="presentation"><a href="/repositories">
                     <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
           <% else %>
             <li><a href="<%= arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
           <% end %>
+
+          <li class="dropdown help-menu">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="arv-help">
+              <span class="fa fa-lg fa-question-circle"></span>
+            </a>
+            <ul class="dropdown-menu">
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+            </ul>
+          </li>
         </ul>
       </div><!-- /.navbar-collapse -->
     </nav>
index 86eab623f6595508043ee31634580435056caceb..fc67e1791a93954ac447c25fa17f21c234cf1e6a 100644 (file)
@@ -31,7 +31,7 @@
       </th><th>
        Owner
       </th><th>
-       Age
+       Created at
       </th><th>
       </th>
     </tr>
@@ -52,7 +52,7 @@
       </td><td>
         <%= link_to_if_arvados_object ob.owner_uuid, friendly_name: true %>
       </td><td>
-        <%= distance_of_time_in_words(ob.created_at, Time.now) %>
+        <%= ob.created_at.to_s %>
       </td><td>
         <%= render partial: 'delete_object_button', locals: {object:ob} %>
       </td>
index 1624fd7eb01d4f9cb56a2627fee1ffb1ac83928e..bd4870000366b617726663a2291c215a89ad8a7a 100644 (file)
@@ -1,8 +1,18 @@
 <% content_for :tab_line_buttons do %>
-  <%= form_tag '/pipeline_instances' do |f| %>
-  <%= hidden_field :pipeline_instance, :pipeline_template_uuid, :value => @object.uuid %>
-  <%= button_tag "Run this pipeline", {class: 'btn btn-primary pull-right', id: "run-pipeline-button"} %>
-<% end %>
+  <%= button_to(choose_projects_path(id: "run-pipeline-button",
+                                     title: 'Choose project',
+                                     editable: true,
+                                     action_name: 'Choose',
+                                     action_href: pipeline_instances_path,
+                                     action_method: 'post',
+                                     action_data: {selection_param: 'pipeline_instance[owner_uuid]',
+                                                   'pipeline_instance[pipeline_template_uuid]' => @object.uuid,
+                                                   'success' => 'redirect-to-created-object'
+                                                  }.to_json),
+                { class: "btn btn-primary btn-sm", remote: true, method: 'get' }
+               ) do %>
+                   Run this pipeline 
+                 <% end %>
 <% end %>
 
 <%= render partial: 'pipeline_instances/show_components_editable', locals: {editable: false} %>
index f18f515ac3533388c3eda2bf43e400ca73036909..9a02e0c337ac1eb1ea554a802535d7e14aa45f66 100644 (file)
 
     <tr>
       <td>
-        <%= form_tag '/pipeline_instances' do |f| %>
-          <%= hidden_field :pipeline_instance, :pipeline_template_uuid, :value => ob.uuid %>
-          <%= button_tag nil, {class: "btn btn-default btn-xs", title: "Run #{ob.name}"} do %>
-            Run <i class="fa fa-fw fa-play"></i>
-          <% end %>
-        <% end %>
+        <%= button_to(choose_projects_path(id: "run-pipeline-button",
+                                     title: 'Choose project',
+                                     editable: true,
+                                     action_name: 'Choose',
+                                     action_href: pipeline_instances_path,
+                                     action_method: 'post',
+                                     action_data: {selection_param: 'pipeline_instance[owner_uuid]',
+                                                   'pipeline_instance[pipeline_template_uuid]' => ob.uuid,
+                                                   'success' => 'redirect-to-created-object'
+                                                  }.to_json),
+                { class: "btn btn-default btn-xs", title: "Run #{ob.name}", remote: true, method: 'get' }
+            ) do %>
+               <i class="fa fa-fw fa-play"></i> Run
+              <% end %>
       </td>
       <td>
         <%= render :partial => "show_object_button", :locals => {object: ob, size: 'xs'} %>
index a8c00e75442e76b0925a088454177bc90ce8a4ad..ebb52019a9f57d3912c7cc4c9b2383eec689d9b6 100644 (file)
@@ -20,7 +20,7 @@
           <th>Script</th>
           <th>Output</th>
           <th>Log</th>
-          <th>Age</th>
+          <th>Created at</th>
           <th>Status</th>
           <th>Progress</th>
         </tr>
@@ -92,7 +92,7 @@
 
 <td>
   <small>
-    <%= raw(distance_of_time_in_words(j.created_at, Time.now).sub('about ','~').sub(' ','&nbsp;')) if j.created_at %>
+    <%= j.created_at.to_s if j.created_at %>
   </small>
 </td>
 
       <tr>
         <th>Instance</th>
         <th>Template</th>
-        <th>Age</th>
+        <th>Created at</th>
         <th>Status</th>
         <th>Progress</th>
       </tr>
 
           <td>
             <small>
-              <%= raw(distance_of_time_in_words(p.created_at, Time.now).sub('about ','~').sub(' ','&nbsp;')) if p.created_at %>
+              <%= (p.created_at.to_s) if p.created_at %>
             </small>
           </td>
 
           </td>
           <td>
             <small>
-              <%= raw(distance_of_time_in_words(c.created_at, Time.now).sub('about ','~').sub(' ','&nbsp;')) if c.created_at %>
+              <%= c.created_at.to_s if c.created_at %>
             </small>
           </td>
           <td>
index f3dbcb523be5d2a63d1c197dc938706811aa7dea..50f990aa0bca1119c11aba9b60a5365fdd6f8811 100644 (file)
@@ -296,4 +296,11 @@ class ApplicationControllerTest < ActionController::TestCase
     assert users.size == 3, 'Expected two objects in the preloaded hash'
   end
 
+  test "requesting a nonexistent object returns 404" do
+    # We're really testing ApplicationController's find_object_by_uuid.
+    # It's easiest to do that by instantiating a concrete controller.
+    @controller = NodesController.new
+    get(:show, {id: "zzzzz-zzzzz-zzzzzzzzzzzzzzz"}, session_for(:admin))
+    assert_response 404
+  end
 end
index babe4fbf931542b7dc0c46d79afbb6874b8d198e..4745d1b9ac7a8f7860e3c10b5b86299f404c7d07 100644 (file)
@@ -1,6 +1,8 @@
 require 'test_helper'
 
 class CollectionsControllerTest < ActionController::TestCase
+  NONEXISTENT_COLLECTION = "ffffffffffffffffffffffffffffffff+0"
+
   def collection_params(collection_name, file_name=nil)
     uuid = api_fixture('collections')[collection_name.to_s]['uuid']
     params = {uuid: uuid, id: uuid}
@@ -173,4 +175,9 @@ class CollectionsControllerTest < ActionController::TestCase
                "when showing the user agreement.")
     assert_response :success
   end
+
+  test "requesting nonexistent Collection returns 404" do
+    show_collection({uuid: NONEXISTENT_COLLECTION, id: NONEXISTENT_COLLECTION},
+                    :active, 404)
+  end
 end
index bf21a26435583dbb15d3097528e83003335e67c3..86778df03607d3bdec23e81dabc0c804c6874457 100644 (file)
@@ -7,7 +7,7 @@ class UsersControllerTest < ActionController::TestCase
   end
 
   test "ignore previously valid token (for deleted user), don't crash" do
-    get :welcome, {}, session_for(:valid_token_deleted_user)
+    get :activity, {}, session_for(:valid_token_deleted_user)
     assert_response :redirect
     assert_match /^#{Rails.configuration.arvados_login_base}/, @response.redirect_url
     assert_nil assigns(:my_jobs)
index 092041d512728e6f3289f28bcc145922642cf3ca..d64ce35ecdeca54ae07cc33e0edafcfca9fa8646 100644 (file)
@@ -1,19 +1,76 @@
 require 'integration_helper'
 
 class ErrorsTest < ActionDispatch::IntegrationTest
-  BAD_UUID = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+  BAD_UUID = "ffffffffffffffffffffffffffffffff+0"
 
   test "error page renders user navigation" do
     visit(page_with_token("active", "/collections/#{BAD_UUID}"))
-    assert(page.has_text?(@@API_AUTHS["active"]["email"]),
+    assert(page.has_text?(api_fixture("users")["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 "no user navigation with expired token" do
+    visit(page_with_token("expired", "/collections/#{BAD_UUID}"))
+    assert(page.has_no_text?(api_fixture("users")["active"]["email"]),
+           "Page visited with expired token included user information")
+    assert(page.has_selector?("a", text: /log ?in/i),
+           "Login prompt missing on expired token 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
+
+  test "'object not found' page includes search link" do
+    visit(page_with_token("active", "/collections/#{BAD_UUID}"))
+    assert(all("a").any? { |a| a[:href] =~ %r{/collections/?(\?|$)} },
+           "no search link found on 404 page")
+  end
+
+  def now_timestamp
+    Time.now.utc.to_i
+  end
+
+  def page_has_error_token?(start_stamp)
+    matching_stamps = (start_stamp .. now_timestamp).to_a.join("|")
+    # Check the page HTML because we really don't care how it's presented.
+    # I think it would even be reasonable to put it in a comment.
+    page.html =~ /\b(#{matching_stamps})\+[0-9A-Fa-f]{8}\b/
+  end
+
+  # We use API tokens with limited scopes as the quickest way to get the API
+  # server to return an error.  If Workbench gets smarter about coping when
+  # it has a too-limited token, these tests will need to be adjusted.
+  test "API error page includes error token" do
+    start_stamp = now_timestamp
+    visit(page_with_token("active_readonly", "/authorized_keys"))
+    click_on "Add a new authorized key"
+    assert(page.has_text?(/fiddlesticks/i),
+           "Not on an error page after making an SSH key out of scope")
+    assert(page_has_error_token?(start_stamp), "no error token on 404 page")
+  end
+
+  test "showing a bad UUID returns 404" do
+    visit(page_with_token("active", "/pipeline_templates/zzz"))
+    assert(page.has_no_text?(/fiddlesticks/i),
+           "trying to show a bad UUID rendered a fiddlesticks page, not 404")
+  end
+
+  test "404 page includes information about missing object" do
+    visit(page_with_token("active", "/groups/zazazaz"))
+    assert(page.has_text?(/group with UUID zazazaz/i),
+           "name of searched group missing from 404 page")
+  end
+
+  test "unrouted 404 page works" do
+    visit(page_with_token("active", "/__asdf/ghjk/zxcv"))
+    assert(page.has_text?(/not found/i),
+           "unrouted page missing 404 text")
+    assert(page.has_no_text?(/fiddlesticks/i),
+           "unrouted request returned a generic error page, not 404")
+  end
 end
index e33944563ab58c0e7c75c946f961d792a008924b..5071fb47666bee5fe73813a3e50cc93d9b3580a2 100644 (file)
@@ -18,18 +18,17 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
       find('a,button', text: 'Run').click
     end
 
+    # project chooser
+    within('.modal-dialog') do
+      find('.selectable', text: 'A Project').click
+      find('button', text: 'Choose').click
+    end
+
     # This pipeline needs input. So, Run should be disabled
     page.assert_selector 'a.disabled,button.disabled', text: 'Run'
 
     instance_page = current_path
 
-    # put this pipeline instance in "A Project"
-    find('button', text: 'Choose a project...').click
-    within('.modal-dialog') do
-      find('.selectable', text: 'A Project').click
-      find('button', text: 'Move').click
-    end
-
     # Go over to the collections page and select something
     visit '/collections'
     within('tr', text: 'GNU_General_Public_License') do
index 36f3654573a0e9761a8012b17384e0919cfb992f..81fa12f643da9dd42e5b1f83b0e4f29653133d46 100644 (file)
@@ -26,6 +26,8 @@ BASE_DEPS = base/Dockerfile $(BASE_GENERATED)
 
 JOBS_DEPS = jobs/Dockerfile
 
+BWA_SAMTOOLS_DEPS = bwa-samtools/Dockerfile
+
 API_DEPS = api/Dockerfile $(API_GENERATED)
 
 DOC_DEPS = doc/Dockerfile doc/apache2_vhost
@@ -130,6 +132,10 @@ jobs-image: base-image $(BUILD) $(JOBS_DEPS)
        $(DOCKER_BUILD) -t arvados/jobs jobs
        date >jobs-image
 
+bwa-samtools-image: jobs-image $(BUILD) $(BWA_SAMTOOLS_DEPS)
+       $(DOCKER_BUILD) -t arvados/jobs-bwa-samtools bwa-samtools
+       date >bwa-samtools-image
+
 workbench-image: passenger-image $(BUILD) $(WORKBENCH_DEPS)
        mkdir -p workbench/generated
        tar -czf workbench/generated/workbench.tar.gz -C build/apps workbench
diff --git a/docker/bwa-samtools/Dockerfile b/docker/bwa-samtools/Dockerfile
new file mode 100644 (file)
index 0000000..cf19ee9
--- /dev/null
@@ -0,0 +1,21 @@
+FROM arvados/jobs
+MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+
+USER root
+
+RUN cd /tmp && \
+    curl --location http://downloads.sourceforge.net/project/bio-bwa/bwa-0.7.9a.tar.bz2 -o bwa-0.7.9a.tar.bz2 && \
+    tar xjf bwa-0.7.9a.tar.bz2 && \
+    cd bwa-0.7.9a && \
+    make && \
+    (find . -executable -type f -print0 | xargs -0 -I {} mv {} /usr/local/bin) && \
+    rm -r /tmp/bwa-0.7.9a* && \
+    cd /tmp && \
+    curl --location http://downloads.sourceforge.net/project/samtools/samtools/0.1.19/samtools-0.1.19.tar.bz2 -o samtools-0.1.19.tar.bz2 && \
+    tar xjf samtools-0.1.19.tar.bz2 && \
+    cd samtools-0.1.19 && \
+    make && \
+    (find . -executable -type f -print0 | xargs -0 -I {} mv {} /usr/local/bin) && \
+    rm -r /tmp/samtools-0.1.19*
+
+USER crunch
\ No newline at end of file
index 0d9051c8315cc6ca8300cd1f56dac31ab043d899..e84150a35d9b4064264393cdbab6e2e5312bc8f7 100755 (executable)
@@ -56,14 +56,20 @@ class Google::APIClient
    return @discovery_documents["#{api}:#{version}"] ||=
      begin
        # fetch new API discovery doc if stale
-       cached_doc = File.expand_path '~/.cache/arvados/discovery_uri.json'
-       if not File.exist?(cached_doc) or (Time.now - File.mtime(cached_doc)) > 86400
+       cached_doc = File.expand_path '~/.cache/arvados/discovery_uri.json' rescue nil
+
+       if cached_doc.nil? or not File.exist?(cached_doc) or (Time.now - File.mtime(cached_doc)) > 86400
          response = self.execute!(:http_method => :get,
                                   :uri => self.discovery_uri(api, version),
                                   :authenticated => false)
-         FileUtils.makedirs(File.dirname cached_doc)
-         File.open(cached_doc, 'w') do |f|
-           f.puts response.body
+
+         begin
+           FileUtils.makedirs(File.dirname cached_doc)
+           File.open(cached_doc, 'w') do |f|
+             f.puts response.body
+           end
+         rescue
+           return JSON.load response.body
          end
        end
 
@@ -85,8 +91,8 @@ end
 def init_config
   # read authentication data from arvados configuration file if present
   lineno = 0
-  config_file = File.expand_path('~/.config/arvados/settings.conf')
-  if File.exist? config_file then
+  config_file = File.expand_path('~/.config/arvados/settings.conf') rescue nil
+  if not config_file.nil? and File.exist? config_file then
     File.open(config_file, 'r').each do |line|
       lineno = lineno + 1
       # skip comments
index 219c315c4cfbf24a5d1c988202e58abb276a961f..b0d779bf3ce84ba3ac9effd9839bd49a82bf9b77 100755 (executable)
@@ -622,6 +622,7 @@ for (my $todo_ptr = 0; $todo_ptr <= $#jobstep_todo; $todo_ptr ++)
     $ENV{"TASK_SLOT_NODE"} = $slot[$childslot]->{node}->{name};
     $ENV{"TASK_SLOT_NUMBER"} = $slot[$childslot]->{cpu};
     $ENV{"TASK_WORK"} = $ENV{"JOB_WORK"}."/$id.$$";
+    $ENV{"HOME"} = $ENV{"TASK_WORK"};
     $ENV{"TASK_KEEPMOUNT"} = $ENV{"TASK_WORK"}.".keep";
     $ENV{"TASK_TMPDIR"} = $ENV{"TASK_WORK"}; # deprecated
     $ENV{"CRUNCH_NODE_SLOTS"} = $slot[$childslot]->{node}->{ncpus};
@@ -661,6 +662,7 @@ for (my $todo_ptr = 0; $todo_ptr <= $#jobstep_todo; $todo_ptr ++)
       $command .= "-v \Q$ENV{TASK_WORK}:/tmp/crunch-job:rw\E ";
       $command .= "-v \Q$ENV{CRUNCH_SRC}:/tmp/crunch-src:ro\E ";
       $command .= "-v \Q$ENV{TASK_KEEPMOUNT}:/mnt:ro\E ";
+      $command .= "-e \QHOME=/tmp/crunch-job\E ";
       while (my ($env_key, $env_val) = each %ENV)
       {
         if ($env_key =~ /^(ARVADOS|JOB|TASK)_/) {
index 905cce14db375dc93cef8b282c672ebf135739e4..3290602cce1a389520f324595bad5f4ce690a99c 100644 (file)
@@ -70,7 +70,7 @@ gem 'database_cleaner'
 
 gem 'themes_for_rails'
 
-gem 'arvados-cli', '>= 0.1.20140627084759'
+gem 'arvados-cli', '>= 0.1.20140630151639'
 
 # pg_power lets us use partial indexes in schema.rb in Rails 3
 gem 'pg_power'
index a38f3ac7c3c4be54478f12a5c7c820cc03f53eca..3167a75ba77d4fae8bd29f96b78d50710f6201ed 100644 (file)
@@ -35,12 +35,12 @@ GEM
     addressable (2.3.6)
     andand (1.3.3)
     arel (3.0.3)
-    arvados (0.1.20140627084759)
+    arvados (0.1.20140630151639)
       activesupport (>= 3.2.13)
       andand
       google-api-client (~> 0.6.3)
       json (>= 1.7.7)
-    arvados-cli (0.1.20140627084759)
+    arvados-cli (0.1.20140630151639)
       activesupport (~> 3.2, >= 3.2.13)
       andand (~> 1.3, >= 1.3.3)
       arvados (~> 0.1.0)
@@ -215,7 +215,7 @@ PLATFORMS
 DEPENDENCIES
   acts_as_api
   andand
-  arvados-cli (>= 0.1.20140627084759)
+  arvados-cli (>= 0.1.20140630151639)
   coffee-rails (~> 3.2.0)
   database_cleaner
   faye-websocket
index fe5598e0dc5ac36797c6a65e9729bbe1e29b6e0e..7e5b316c2d4bea89706896274e7da19ec22b245c 100644 (file)
@@ -20,12 +20,11 @@ class ApplicationController < ActionController::Base
   include LoadParam
   include RecordFilters
 
-  ERROR_ACTIONS = [:render_error, :render_not_found]
-
-
   respond_to :json
   protect_from_forgery
 
+  ERROR_ACTIONS = [:render_error, :render_not_found]
+
   before_filter :respond_with_json_by_default
   before_filter :remote_ip
   before_filter :load_read_auths
@@ -46,6 +45,17 @@ class ApplicationController < ActionController::Base
 
   attr_accessor :resource_attrs
 
+  begin
+    rescue_from(Exception,
+                ArvadosModel::PermissionDeniedError,
+                :with => :render_error)
+    rescue_from(ActiveRecord::RecordNotFound,
+                ActionController::RoutingError,
+                ActionController::UnknownController,
+                AbstractController::ActionNotFound,
+                :with => :render_not_found)
+  end
+
   def index
     @objects.uniq!(&:id) if @select.nil? or @select.include? "id"
     if params[:eager] and params[:eager] != '0' and params[:eager] != 0 and params[:eager] != ''
@@ -85,21 +95,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  begin
-    rescue_from Exception,
-    :with => :render_error
-    rescue_from ActiveRecord::RecordNotFound,
-    :with => :render_not_found
-    rescue_from ActionController::RoutingError,
-    :with => :render_not_found
-    rescue_from ActionController::UnknownController,
-    :with => :render_not_found
-    rescue_from AbstractController::ActionNotFound,
-    :with => :render_not_found
-    rescue_from ArvadosModel::PermissionDeniedError,
-    :with => :render_error
-  end
-
   def render_404_if_no_object
     render_not_found "Object not found" if !@object
   end
@@ -115,16 +110,29 @@ class ApplicationController < ActionController::Base
       errors = [e.inspect]
     end
     status = e.respond_to?(:http_status) ? e.http_status : 422
-    render json: { errors: errors }, status: status
+    send_error(*errors, status: status)
   end
 
   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
     logger.error e.inspect
-    render json: { errors: ["Path not found"] }, status: 404
+    send_error("Path not found", status: 404)
   end
 
   protected
 
+  def send_error(*args)
+    if args.last.is_a? Hash
+      err = args.pop
+    else
+      err = {}
+    end
+    err[:errors] ||= args
+    err[:error_token] = [Time.now.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
+    status = err.delete(:status) || 422
+    logger.error "Error #{err[:error_token]}: #{status}"
+    render json: err, status: status
+  end
+
   def find_objects_for_index
     @objects ||= model_class.readable_by(*@read_users)
     apply_where_limit_order_params
@@ -247,12 +255,8 @@ class ApplicationController < ActionController::Base
   def require_login
     if not current_user
       respond_to do |format|
-        format.json {
-          render :json => { errors: ['Not logged in'] }.to_json, status: 401
-        }
-        format.html {
-          redirect_to '/auth/joshid'
-        }
+        format.json { send_error("Not logged in", status: 401) }
+        format.html { redirect_to '/auth/joshid' }
       end
       false
     end
@@ -260,14 +264,14 @@ class ApplicationController < ActionController::Base
 
   def admin_required
     unless current_user and current_user.is_admin
-      render :json => { errors: ['Forbidden'] }.to_json, status: 403
+      send_error("Forbidden", status: 403)
     end
   end
 
   def require_auth_scope
     if @read_auths.empty?
       if require_login != false
-        render :json => { errors: ['Forbidden'] }.to_json, status: 403
+        send_error("Forbidden", status: 403)
       end
       false
     end
index 76a228d9d580c21d3483e9a01643255d67c012c4..f365a7fee8996e7c9ba51cdd11bc4d525c3e9193 100644 (file)
@@ -82,7 +82,8 @@ class Arvados::V1::ApiClientAuthorizationsController < ApplicationController
 
   def current_api_client_is_trusted
     unless Thread.current[:api_client].andand.is_trusted
-      render :json => { errors: ['Forbidden: this API client cannot manipulate other clients\' access tokens.'] }.to_json, status: 403
+      send_error('Forbidden: this API client cannot manipulate other clients\' access tokens.',
+                 status: 403)
     end
   end
 end
index d38f257db9fd6cc4de53c10c699eb4b1b5171695..feeb82d9a16b41acedf0f8256eb894acf19f2490 100644 (file)
@@ -8,9 +8,8 @@ class Arvados::V1::JobsController < ApplicationController
   def create
     [:repository, :script, :script_version, :script_parameters].each do |r|
       if !resource_attrs[r]
-        return render json: {
-          :errors => ["#{r} attribute must be specified"]
-        }, status: :unprocessable_entity
+        return send_error("#{r} attribute must be specified",
+                          status: :unprocessable_entity)
       end
     end
 
index 188ecfc1a04a78731697a55da0ad05a5d8706476..0772227adca9c0ffa3ac6d541209be8bcf6cecad 100644 (file)
@@ -2,7 +2,8 @@ class Arvados::V1::LinksController < ApplicationController
 
   def check_uuid_kind uuid, kind
     if kind and ArvadosModel::resource_class_for_uuid(uuid).andand.kind != kind
-      render :json => { errors: ["'#{kind}' does not match uuid '#{uuid}', expected '#{ArvadosModel::resource_class_for_uuid(uuid).andand.kind}'"] }.to_json, status: 422
+      send_error("'#{kind}' does not match uuid '#{uuid}', expected '#{ArvadosModel::resource_class_for_uuid(uuid).andand.kind}'",
+                 status: 422)
       nil
     else
       true
index 52dd8d79ff47014a9bfe0896a818b28c4d83395d..677685d67abdb60270b113ffeb46d6bb5edea81c 100644 (file)
@@ -41,7 +41,11 @@ class User < ArvadosModel
   end
 
   def groups_i_can(verb)
-    self.group_permissions.select { |uuid, mask| mask[verb] }.keys
+    my_groups = self.group_permissions.select { |uuid, mask| mask[verb] }.keys
+    if verb == :read
+      my_groups << anonymous_group_uuid
+    end
+    my_groups
   end
 
   def can?(actions)
diff --git a/services/api/db/migrate/20140627210837_anonymous_group.rb b/services/api/db/migrate/20140627210837_anonymous_group.rb
new file mode 100644 (file)
index 0000000..0bb7608
--- /dev/null
@@ -0,0 +1,17 @@
+class AnonymousGroup < ActiveRecord::Migration
+  include CurrentApiClient
+
+  def up
+    # create the anonymous group and user
+    anonymous_group
+    anonymous_user
+  end
+
+  def down
+    act_as_system_user do
+      anonymous_user.destroy
+      anonymous_group.destroy
+    end
+  end
+
+end
index 1f9b5019ab60a528ba72434d4d91190783195bef..256fa3977eb3aa458be00b1805fc40a1c6f68cdf 100644 (file)
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20140611173003) do
+ActiveRecord::Schema.define(:version => 20140627210837) do
 
 
 
index 1f17bc84e63c0cb5a2734762e34510ac4324448b..abd325c724267c76a5a4e11e696bdc62c60ee483 100644 (file)
@@ -2,8 +2,10 @@
 #
 # It is invoked by `rake db:seed` and `rake db:setup`.
 
-# These two methods would create the system user and group objects on
-# demand later anyway, but it's better form to create them up front.
+# These two methods would create these objects on demand
+# later anyway, but it's better form to create them up front.
 include CurrentApiClient
 system_user
 system_group
+anonymous_group
+anonymous_user
index f851c588c7445ef7d180bee1896b9f2e0bfc7d36..94bd2b56a887999fa73b6603944a45136cef4bd9 100644 (file)
@@ -41,6 +41,18 @@ module CurrentApiClient
      '000000000000000'].join('-')
   end
 
+  def anonymous_group_uuid
+    [Server::Application.config.uuid_prefix,
+     Group.uuid_prefix,
+     'anonymouspublic'].join('-')
+  end
+
+  def anonymous_user_uuid
+    [Server::Application.config.uuid_prefix,
+     User.uuid_prefix,
+     'anonymouspublic'].join('-')
+  end
+
   def system_user
     if not $system_user
       real_current_user = Thread.current[:user]
@@ -99,4 +111,51 @@ module CurrentApiClient
       Thread.current[:user] = system_user
     end
   end
+
+  def anonymous_group
+    if not $anonymous_group
+      act_as_system_user do
+        ActiveRecord::Base.transaction do
+          $anonymous_group = Group.
+          where(uuid: anonymous_group_uuid).first_or_create do |g|
+            g.update_attributes(name: "Anonymous group",
+                                description: "Anonymous group")
+          end
+        end
+      end
+    end
+    $anonymous_group
+  end
+
+  def anonymous_user
+    if not $anonymous_user
+      act_as_system_user do
+        $anonymous_user = User.where('uuid=?', anonymous_user_uuid).first
+        if !$anonymous_user
+          $anonymous_user = User.new(uuid: anonymous_user_uuid,
+                                     is_active: false,
+                                     is_admin: false,
+                                     email: 'anonymouspublic',
+                                     first_name: 'anonymouspublic',
+                                     last_name: 'anonymouspublic')
+          $anonymous_user.save!
+          $anonymous_user.reload
+        end
+
+        group_perms = Link.where(tail_uuid: anonymous_user_uuid,
+                                 head_uuid: anonymous_group_uuid,
+                                 link_class: 'permission',
+                                 name: 'can_read')
+
+        if !group_perms.any?
+          group_perm = Link.create!(tail_uuid: anonymous_user_uuid,
+                                    head_uuid: anonymous_group_uuid,
+                                    link_class: 'permission',
+                                    name: 'can_read')
+        end
+      end
+    end
+    $anonymous_user
+  end
+
 end
index b1c0e7d316f2a245c82f727406b31f8490ed4e0b..c39c8ea9922f9708028eadbddce1cbc90eae4635 100755 (executable)
@@ -230,7 +230,7 @@ class Dispatcher
         next
       end
 
-      $stderr.puts `cd #{arvados_internal.shellescape} && git fetch --no-tags #{src_repo.shellescape} && git tag #{job.uuid.shellescape} #{job.script_version.shellescape}`
+      $stderr.puts `cd #{arvados_internal.shellescape} && git fetch-pack --all #{src_repo.shellescape} && git tag #{job.uuid.shellescape} #{job.script_version.shellescape}`
 
       cmd_args << crunch_job_bin
       cmd_args << '--job-api-token'
diff --git a/services/api/script/get_anonymous_user_token.rb b/services/api/script/get_anonymous_user_token.rb
new file mode 100755 (executable)
index 0000000..6964af0
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/env ruby
+
+# Get or Create an anonymous user token.
+# If get option is used, an existing anonymous user token is returned. If none exist, one is created.
+# If the get option is omitted, a new token is created and returned.
+
+require 'trollop'
+
+opts = Trollop::options do
+  banner ''
+  banner "Usage: get_anonymous_user_token "
+  banner ''
+  opt :get, <<-eos
+Get an existing anonymous user token. If no such token exists \
+or if this option is omitted, a new token is created and returned.
+  eos
+end
+
+get_existing = opts[:get]
+
+require File.dirname(__FILE__) + '/../config/environment'
+
+include ApplicationHelper
+act_as_system_user
+
+def create_api_client_auth
+  api_client_auth = ApiClientAuthorization.
+    new(user: anonymous_user,
+        api_client_id: 0,
+        expires_at: Time.now + 100.years,
+        scopes: ['GET /'])
+  api_client_auth.save!
+  api_client_auth.reload
+end
+
+if get_existing
+  api_client_auth = ApiClientAuthorization.
+    where('user_id=?', anonymous_user.id.to_i).
+    where('expires_at>?', Time.now).
+    select { |auth| auth.scopes == ['GET /'] }.
+    first
+end
+
+# either not a get or no api_client_auth was found
+if !api_client_auth
+  api_client_auth = create_api_client_auth
+end
+
+# print it to the console
+puts api_client_auth.api_token
index 71b638806d4188dbf3257cd8851c10dfaa7f1f73..48b4b875e69baf736f55819ceadb4a4053987f69 100644 (file)
@@ -87,6 +87,13 @@ active_apitokens:
   scopes: ["GET /arvados/v1/api_client_authorizations",
            "POST /arvados/v1/api_client_authorizations"]
 
+active_readonly:
+  api_client: untrusted
+  user: active
+  api_token: activereadonlyabcdefghijklmnopqrstuvwxyz1234568790
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /"]
+
 spectator:
   api_client: untrusted
   user: spectator
@@ -136,3 +143,10 @@ valid_token_deleted_user:
   user_id: 1234567
   api_token: tewfa58099sndckyqhlgd37za6e47o6h03r9l1vpll23hudm8b
   expires_at: 2038-01-01 00:00:00
+
+anonymous:
+  api_client: untrusted
+  user: anonymous
+  api_token: 4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /"]
index e41ca8a5c5cf4ed9edc4ee407acde14dd7f693f6..cd6157bdd84a6a41be6edf48c9298360ecd98c31 100644 (file)
@@ -87,3 +87,16 @@ bad_group_has_ownership_cycle_b:
   modified_at: 2014-05-03 18:50:08 -0400
   updated_at: 2014-05-03 18:50:08 -0400
   name: Owned by bad group a
+
+anonymous_group:
+  uuid: zzzzz-j7d0g-anonymouspublic
+  owner_uuid: zzzzz-tpzed-000000000000000
+  name: Anonymous group
+  description: Anonymous group
+
+anonymously_accessible_project:
+  uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
+  owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  name: Unrestricted public data
+  group_class: project
+  description: An anonymously accessible project
index b35e7d47844c2933b4b30f0cb41075005919a613..e9cb9efab31bda21a157211dee2a7c7bab6d40a4 100644 (file)
@@ -441,6 +441,45 @@ bug2931_link_with_null_head_uuid:
   head_uuid: ~
   properties: {}
 
+anonymous_group_can_read_anonymously_accessible_project:
+  uuid: zzzzz-o0j2j-15gpzezqjg4bc4z
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-05-30 14:30:00.184389725 Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-000000000000000
+  modified_at: 2014-05-30 14:30:00.184019565 Z
+  updated_at: 2014-05-30 14:30:00.183829316 Z
+  link_class: permission
+  name: can_read
+  tail_uuid: zzzzz-j7d0g-anonymouspublic
+  head_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
+  properties: {}
+
+user_agreement_in_anonymously_accessible_project:
+  uuid: zzzzz-o0j2j-k0ukddp35mt6ok1
+  owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
+  created_at: 2014-06-13 20:42:26 -0800
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-06-13 20:42:26 -0800
+  updated_at: 2014-06-13 20:42:26 -0800
+  link_class: name
+  name: GNU General Public License, version 3
+  tail_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
+  head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
+  properties: {}
+
+user_agreement_readable_by_anonymously_accessible_project:
+  uuid: zzzzz-o0j2j-o5ds5gvhkztdc8h
+  owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
+  created_at: 2014-06-13 20:42:26 -0800
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-06-13 20:42:26 -0800
+  updated_at: 2014-06-13 20:42:26 -0800
+  link_class: permission
+  name: can_read
+
 active_user_permission_to_docker_image_collection:
   uuid: zzzzz-o0j2j-dp1d8395ldqw33s
   owner_uuid: zzzzz-tpzed-000000000000000
index c02ab61aa66b26cec3d96f08f3ad6c2d96fcd630..ac76d5fa47124ef5a5fc17fc1870804b7c532680 100644 (file)
@@ -80,3 +80,11 @@ inactive_but_signed_user_agreement:
   is_admin: false
   prefs: {}
 
+anonymous:
+  uuid: zzzzz-tpzed-anonymouspublic
+  email: anonymouspublic
+  first_name: anonymouspublic
+  last_name: anonymouspublic
+  is_active: false
+  is_admin: false
+  prefs: {}
diff --git a/services/api/test/functional/application_controller_test.rb b/services/api/test/functional/application_controller_test.rb
new file mode 100644 (file)
index 0000000..4144d0a
--- /dev/null
@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class ApplicationControllerTest < ActionController::TestCase
+  BAD_UUID = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+
+  def now_timestamp
+    Time.now.utc.to_i
+  end
+
+  setup do
+    # These tests are meant to check behavior in ApplicationController.
+    # We instantiate a small concrete controller for convenience.
+    @controller = Arvados::V1::SpecimensController.new
+    @start_stamp = now_timestamp
+  end
+
+  def check_error_token
+    token = json_response['error_token']
+    assert_not_nil token
+    token_time = token.split('+', 2).first.to_i
+    assert_operator(token_time, :>=, @start_stamp, "error token too old")
+    assert_operator(token_time, :<=, now_timestamp, "error token too new")
+  end
+
+  def check_404(errmsg="Path not found")
+    assert_response 404
+    assert_equal([errmsg], json_response['errors'])
+    check_error_token
+  end
+
+  test "requesting nonexistent object returns 404 error" do
+    authorize_with :admin
+    get(:show, id: BAD_UUID)
+    check_404
+  end
+
+  test "requesting object without read permission returns 404 error" do
+    authorize_with :spectator
+    get(:show, id: specimens(:owned_by_active_user).uuid)
+    check_404
+  end
+
+  test "submitting bad object returns error" do
+    authorize_with :spectator
+    post(:create, specimen: {badattr: "badvalue"})
+    assert_response 422
+    check_error_token
+  end
+end
index 8f0277909a4126826be910d635ab809c1225a836..d4f25245c31e182a2895a4b4b6480cbaee4f7220 100644 (file)
@@ -60,7 +60,8 @@ class UserTest < ActiveSupport::TestCase
     assert @uninvited_user.can? :write=>"#{@uninvited_user.uuid}"
     assert @uninvited_user.can? :manage=>"#{@uninvited_user.uuid}"
 
-    assert @uninvited_user.groups_i_can(:read).size == 0, "inactive and uninvited user should not be able read any groups"
+    assert @uninvited_user.groups_i_can(:read).size == 1, "inactive and uninvited user can only read anonymous user group"
+    assert @uninvited_user.groups_i_can(:read).first.ends_with? 'anonymouspublic' , "inactive and uninvited user can only read anonymous user group"
     assert @uninvited_user.groups_i_can(:write).size == 0, "inactive and uninvited user should not be able write to any groups"
     assert @uninvited_user.groups_i_can(:manage).size == 0, "inactive and uninvited user should not be able manage any groups"
   end
index 726741e3b01619b79b62094662d76e819e9b38df..b874f5ff5bfcb298a3085ab87d1037b20b398593 100755 (executable)
@@ -43,18 +43,22 @@ collections on the server.""")
     if args.debug:
         arvados.config.settings()['ARVADOS_DEBUG'] = 'true'
 
-    if args.groups:
+    try:
         api = arvados.api('v1')
-        e = operations.inodes.add_entry(GroupsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
-    elif args.tags:
-        api = arvados.api('v1')
-        e = operations.inodes.add_entry(TagsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
-    elif args.collection != None:
-        # Set up the request handler with the collection at the root
-        e = operations.inodes.add_entry(CollectionDirectory(llfuse.ROOT_INODE, operations.inodes, args.collection))
-    else:
-        # Set up the request handler with the 'magic directory' at the root
-        operations.inodes.add_entry(MagicDirectory(llfuse.ROOT_INODE, operations.inodes))
+
+        if args.groups:
+            e = operations.inodes.add_entry(GroupsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
+        elif args.tags:
+            e = operations.inodes.add_entry(TagsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
+        elif args.collection != None:
+            # Set up the request handler with the collection at the root
+            e = operations.inodes.add_entry(CollectionDirectory(llfuse.ROOT_INODE, operations.inodes, args.collection))
+        else:
+            # Set up the request handler with the 'magic directory' at the root
+            operations.inodes.add_entry(MagicDirectory(llfuse.ROOT_INODE, operations.inodes))
+    except Exception as ex:
+        print("arv-mount: %s" % ex)
+        exit(1)
 
     # FUSE options, see mount.fuse(8)
     opts = [optname for optname in ['allow_other', 'debug']