Merge branch '2640-folder-api' into 1970-folder-view
[arvados.git] / services / api / app / controllers / application_controller.rb
index f83cd3428f5e644d4b68793ab3ef0c8e8d23c5f9..8f3a6bc71bc02a5533c89303acd06ad432921693 100644 (file)
@@ -1,13 +1,17 @@
+require 'load_param'
+require 'record_filters'
+
 class ApplicationController < ActionController::Base
   include CurrentApiClient
   include ThemesForRails::ActionController
+  include LoadParam
+  include RecordFilters
 
   ERROR_ACTIONS = [:render_error, :render_not_found]
 
   respond_to :json
   protect_from_forgery
 
-  around_filter :thread_with_auth_info, except: ERROR_ACTIONS
   before_filter :respond_with_json_by_default
   before_filter :remote_ip
   before_filter :load_read_auths
@@ -16,9 +20,9 @@ class ApplicationController < ActionController::Base
   before_filter :catch_redirect_hint
   before_filter(:find_object_by_uuid,
                 except: [:index, :create] + ERROR_ACTIONS)
-  before_filter :load_limit_offset_order_params, only: [:index, :owned_items]
-  before_filter :load_where_param, only: [:index, :owned_items]
-  before_filter :load_filters_param, only: [:index, :owned_items]
+  before_filter :load_limit_offset_order_params, only: [:index, :contents]
+  before_filter :load_where_param, only: [:index, :contents]
+  before_filter :load_filters_param, only: [:index, :contents]
   before_filter :find_objects_for_index, :only => :index
   before_filter :reload_object_before_update, :only => :update
   before_filter(:render_404_if_no_object,
@@ -28,8 +32,6 @@ class ApplicationController < ActionController::Base
 
   attr_accessor :resource_attrs
 
-  DEFAULT_LIMIT = 100
-
   def index
     @objects.uniq!(&:id)
     if params[:eager] and params[:eager] != '0' and params[:eager] != 0 and params[:eager] != ''
@@ -61,7 +63,7 @@ class ApplicationController < ActionController::Base
     show
   end
 
-  def self._owned_items_requires_parameters
+  def self._contents_requires_parameters
     _index_requires_parameters.
       merge({
               include_linked: {
@@ -70,7 +72,7 @@ class ApplicationController < ActionController::Base
             })
   end
 
-  def owned_items
+  def contents
     all_objects = []
     all_available = 0
 
@@ -81,31 +83,23 @@ class ApplicationController < ActionController::Base
     offset_all = @offset
     @orders = []
 
-    ArvadosModel.descendants.
-      reject(&:abstract_class?).
-      sort_by(&:to_s).
+    ArvadosModel.descendants.reject(&:abstract_class?).sort_by(&:to_s).
       each do |klass|
       case klass.to_s
         # We might expect klass==Link etc. here, but we would be
         # disappointed: when Rails reloads model classes, we get two
         # distinct classes called Link which do not equal each
         # other. But we can still rely on klass.to_s to be "Link".
-      when 'ApiClientAuthorization'
+      when 'ApiClientAuthorization', 'UserAgreement', 'Link'
         # Do not want.
       else
         @objects = klass.readable_by(*@read_users)
         cond_sql = "#{klass.table_name}.owner_uuid = ?"
         cond_params = [@object.uuid]
         if params[:include_linked]
-          @objects = @objects.
-            joins("LEFT JOIN links mng_links"\
-                  " ON mng_links.link_class=#{klass.sanitize 'permission'}"\
-                  "    AND mng_links.name=#{klass.sanitize 'can_manage'}"\
-                  "    AND mng_links.tail_uuid=#{klass.sanitize @object.uuid}"\
-                  "    AND mng_links.head_uuid=#{klass.table_name}.uuid")
-          cond_sql += " OR mng_links.uuid IS NOT NULL"
+          cond_sql += " OR #{klass.table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class=#{klass.sanitize 'name'} AND links.tail_uuid=#{klass.sanitize @object.uuid})"
         end
-        @objects = @objects.where(cond_sql, *cond_params).order(:uuid)
+        @objects = @objects.where(cond_sql, *cond_params).order("#{klass.table_name}.uuid")
         @limit = limit_all - all_objects.count
         apply_where_limit_order_params
         items_available = @objects.
@@ -118,10 +112,16 @@ class ApplicationController < ActionController::Base
       end
     end
     @objects = all_objects || []
+    @links = Link.where('link_class=? and tail_uuid=?'\
+                        ' and head_uuid in (?)',
+                        'name',
+                        @object.uuid,
+                        @objects.collect(&:uuid))
     @object_list = {
       :kind  => "arvados#objectList",
       :etag => "",
       :self_link => "",
+      :links => @links.as_api_response(nil),
       :offset => offset_all,
       :limit => limit_all,
       :items_available => all_available,
@@ -178,77 +178,6 @@ class ApplicationController < ActionController::Base
 
   protected
 
-  def load_where_param
-    if params[:where].nil? or params[:where] == ""
-      @where = {}
-    elsif params[:where].is_a? Hash
-      @where = params[:where]
-    elsif params[:where].is_a? String
-      begin
-        @where = Oj.load(params[:where])
-        raise unless @where.is_a? Hash
-      rescue
-        raise ArgumentError.new("Could not parse \"where\" param as an object")
-      end
-    end
-    @where = @where.with_indifferent_access
-  end
-
-  def load_filters_param
-    @filters ||= []
-    if params[:filters].is_a? Array
-      @filters += params[:filters]
-    elsif params[:filters].is_a? String and !params[:filters].empty?
-      begin
-        f = Oj.load params[:filters]
-        raise unless f.is_a? Array
-        @filters += f
-      rescue
-        raise ArgumentError.new("Could not parse \"filters\" param as an array")
-      end
-    end
-  end
-
-  def default_orders
-    ["#{table_name}.modified_at desc"]
-  end
-
-  def load_limit_offset_order_params
-    if params[:limit]
-      unless params[:limit].to_s.match(/^\d+$/)
-        raise ArgumentError.new("Invalid value for limit parameter")
-      end
-      @limit = params[:limit].to_i
-    else
-      @limit = DEFAULT_LIMIT
-    end
-
-    if params[:offset]
-      unless params[:offset].to_s.match(/^\d+$/)
-        raise ArgumentError.new("Invalid value for offset parameter")
-      end
-      @offset = params[:offset].to_i
-    else
-      @offset = 0
-    end
-
-    @orders = []
-    if params[:order]
-      params[:order].split(',').each do |order|
-        attr, direction = order.strip.split " "
-        direction ||= 'asc'
-        if attr.match /^[a-z][_a-z0-9]+$/ and
-            model_class.columns.collect(&:name).index(attr) and
-            ['asc','desc'].index direction.downcase
-          @orders << "#{table_name}.#{attr} #{direction.downcase}"
-        end
-      end
-    end
-    if @orders.empty?
-      @orders = default_orders
-    end
-  end
-
   def find_objects_for_index
     @objects ||= model_class.readable_by(*@read_users)
     apply_where_limit_order_params
@@ -256,62 +185,12 @@ class ApplicationController < ActionController::Base
 
   def apply_where_limit_order_params
     ar_table_name = @objects.table_name
-    if @filters.is_a? Array and @filters.any?
-      cond_out = []
-      param_out = []
-      @filters.each do |filter|
-        attr, operator, operand = filter
-        if !filter.is_a? Array
-          raise ArgumentError.new("Invalid element in filters array: #{filter.inspect} is not an array")
-        elsif !operator.is_a? String
-          raise ArgumentError.new("Invalid operator '#{operator}' (#{operator.class}) in filter")
-        elsif !model_class.searchable_columns(operator).index attr.to_s
-          raise ArgumentError.new("Invalid attribute '#{attr}' in filter")
-        end
-        case operator.downcase
-        when '=', '<', '<=', '>', '>=', 'like'
-          if operand.is_a? String
-            cond_out << "#{ar_table_name}.#{attr} #{operator} ?"
-            if (# any operator that operates on value rather than
-                # representation:
-                operator.match(/[<=>]/) and
-                model_class.attribute_column(attr).type == :datetime)
-              operand = Time.parse operand
-            end
-            param_out << operand
-          elsif operand.nil? and operator == '='
-            cond_out << "#{ar_table_name}.#{attr} is null"
-          else
-            raise ArgumentError.new("Invalid operand type '#{operand.class}' "\
-                                    "for '#{operator}' operator in filters")
-          end
-        when 'in'
-          if operand.is_a? Array
-            cond_out << "#{ar_table_name}.#{attr} IN (?)"
-            param_out << operand
-          else
-            raise ArgumentError.new("Invalid operand type '#{operand.class}' "\
-                                    "for '#{operator}' operator in filters")
-          end
-        when 'is_a'
-          operand = [operand] unless operand.is_a? Array
-          cond = []
-          operand.each do |op|
-              cl = ArvadosModel::kind_class op
-              if cl
-                cond << "#{ar_table_name}.#{attr} like ?"
-                param_out << cl.uuid_like_pattern
-              else
-                cond << "1=0"
-              end
-          end
-          cond_out << cond.join(' OR ')
-        end
-      end
-      if cond_out.any?
-        @objects = @objects.where(cond_out.join(' AND '), *param_out)
-      end
+
+    ft = record_filters @filters, ar_table_name
+    if ft[:cond_out].any?
+      @objects = @objects.where(ft[:cond_out].join(' AND '), *ft[:param_out])
     end
+
     if @where.is_a? Hash and @where.any?
       conditions = ['1=1']
       @where.each do |attr,value|
@@ -442,63 +321,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def thread_with_auth_info
-    Thread.current[:request_starttime] = Time.now
-    Thread.current[:api_url_base] = root_url.sub(/\/$/,'') + '/arvados/v1'
-    begin
-      user = nil
-      api_client = nil
-      api_client_auth = nil
-      supplied_token =
-        params[:api_token] ||
-        params[:oauth_token] ||
-        request.headers["Authorization"].andand.match(/OAuth2 ([a-z0-9]+)/).andand[1]
-      if supplied_token
-        api_client_auth = ApiClientAuthorization.
-          includes(:api_client, :user).
-          where('api_token=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', supplied_token).
-          first
-        if api_client_auth.andand.user
-          session[:user_id] = api_client_auth.user.id
-          session[:api_client_uuid] = api_client_auth.api_client.andand.uuid
-          session[:api_client_authorization_id] = api_client_auth.id
-          user = api_client_auth.user
-          api_client = api_client_auth.api_client
-        else
-          # Token seems valid, but points to a non-existent (deleted?) user.
-          api_client_auth = nil
-        end
-      elsif session[:user_id]
-        user = User.find(session[:user_id]) rescue nil
-        api_client = ApiClient.
-          where('uuid=?',session[:api_client_uuid]).
-          first rescue nil
-        if session[:api_client_authorization_id] then
-          api_client_auth = ApiClientAuthorization.
-            find session[:api_client_authorization_id]
-        end
-      end
-      Thread.current[:api_client_ip_address] = remote_ip
-      Thread.current[:api_client_authorization] = api_client_auth
-      Thread.current[:api_client_uuid] = api_client.andand.uuid
-      Thread.current[:api_client] = api_client
-      Thread.current[:user] = user
-      if api_client_auth
-        api_client_auth.last_used_at = Time.now
-        api_client_auth.last_used_by_ip_address = remote_ip
-        api_client_auth.save validate: false
-      end
-      yield
-    ensure
-      Thread.current[:api_client_ip_address] = nil
-      Thread.current[:api_client_authorization] = nil
-      Thread.current[:api_client_uuid] = nil
-      Thread.current[:api_client] = nil
-      Thread.current[:user] = nil
-    end
-  end
-  # /Authentication
-
   def respond_with_json_by_default
     html_index = request.accepts.index(Mime::HTML)
     if html_index.nil? or request.accepts[0...html_index].include?(Mime::JSON)