Merge branch '2640-folder-api' into 1970-folder-view
[arvados.git] / services / api / app / controllers / application_controller.rb
index 3891bde52e6995848877be97935db38b44355c30..8f3a6bc71bc02a5533c89303acd06ad432921693 100644 (file)
@@ -7,25 +7,26 @@ class ApplicationController < ActionController::Base
   include LoadParam
   include RecordFilters
 
+  ERROR_ACTIONS = [:render_error, :render_not_found]
+
   respond_to :json
   protect_from_forgery
 
   before_filter :respond_with_json_by_default
   before_filter :remote_ip
-  before_filter :require_auth_scope, :except => :render_not_found
-  before_filter :catch_redirect_hint
+  before_filter :load_read_auths
+  before_filter :require_auth_scope, except: ERROR_ACTIONS
 
-  before_filter :find_object_by_uuid, :except => [:index, :create,
-                                                  :render_error,
-                                                  :render_not_found]
-  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 :catch_redirect_hint
+  before_filter(:find_object_by_uuid,
+                except: [:index, :create] + ERROR_ACTIONS)
+  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, except: [:index, :create,
-                                                   :render_error,
-                                                   :render_not_found]
+  before_filter(:render_404_if_no_object,
+                except: [:index, :create] + ERROR_ACTIONS)
 
   theme :select_theme
 
@@ -62,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: {
@@ -71,7 +72,7 @@ class ApplicationController < ActionController::Base
             })
   end
 
-  def owned_items
+  def contents
     all_objects = []
     all_available = 0
 
@@ -82,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(current_user)
+        @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.
@@ -119,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,
@@ -180,7 +179,7 @@ class ApplicationController < ActionController::Base
   protected
 
   def find_objects_for_index
-    @objects ||= model_class.readable_by(current_user)
+    @objects ||= model_class.readable_by(*@read_users)
     apply_where_limit_order_params
   end
 
@@ -272,15 +271,34 @@ class ApplicationController < ActionController::Base
   end
 
   # Authentication
+  def load_read_auths
+    @read_auths = []
+    if current_api_client_authorization
+      @read_auths << current_api_client_authorization
+    end
+    # Load reader tokens if this is a read request.
+    # If there are too many reader tokens, assume the request is malicious
+    # and ignore it.
+    if request.get? and params[:reader_tokens] and
+        params[:reader_tokens].size < 100
+      @read_auths += ApiClientAuthorization
+        .includes(:user)
+        .where('api_token IN (?) AND
+                (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)',
+               params[:reader_tokens])
+        .all
+    end
+    @read_auths.select! { |auth| auth.scopes_allow_request? request }
+    @read_users = @read_auths.map { |auth| auth.user }.uniq
+  end
+
   def require_login
-    if current_user
-      true
-    else
+    if not current_user
       respond_to do |format|
         format.json {
           render :json => { errors: ['Not logged in'] }.to_json, status: 401
         }
-        format.html  {
+        format.html {
           redirect_to '/auth/joshid'
         }
       end
@@ -295,9 +313,11 @@ class ApplicationController < ActionController::Base
   end
 
   def require_auth_scope
-    return false unless require_login
-    unless current_api_client_auth_has_scope("#{request.method} #{request.path}")
-      render :json => { errors: ['Forbidden'] }.to_json, status: 403
+    if @read_auths.empty?
+      if require_login != false
+        render :json => { errors: ['Forbidden'] }.to_json, status: 403
+      end
+      false
     end
   end
 
@@ -342,28 +362,38 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def self.accept_attribute_as_json(attr, force_class=nil)
-    before_filter lambda { accept_attribute_as_json attr, force_class }
+  def load_json_value(hash, key, must_be_class=nil)
+    if hash[key].is_a? String
+      hash[key] = Oj.load(hash[key], symbol_keys: false)
+      if must_be_class and !hash[key].is_a? must_be_class
+        raise TypeError.new("parameter #{key.to_s} must be a #{must_be_class.to_s}")
+      end
+    end
+  end
+
+  def self.accept_attribute_as_json(attr, must_be_class=nil)
+    before_filter lambda { accept_attribute_as_json attr, must_be_class }
   end
   accept_attribute_as_json :properties, Hash
   accept_attribute_as_json :info, Hash
-  def accept_attribute_as_json(attr, force_class)
+  def accept_attribute_as_json(attr, must_be_class)
     if params[resource_name] and resource_attrs.is_a? Hash
-      if resource_attrs[attr].is_a? String
-        resource_attrs[attr] = Oj.load(resource_attrs[attr],
-                                       symbol_keys: false)
-        if force_class and !resource_attrs[attr].is_a? force_class
-          raise TypeError.new("#{resource_name}[#{attr.to_s}] must be a #{force_class.to_s}")
-        end
-      elsif resource_attrs[attr].is_a? Hash
+      if resource_attrs[attr].is_a? Hash
         # Convert symbol keys to strings (in hashes provided by
         # resource_attrs)
         resource_attrs[attr] = resource_attrs[attr].
           with_indifferent_access.to_hash
+      else
+        load_json_value(resource_attrs, attr, must_be_class)
       end
     end
   end
 
+  def self.accept_param_as_json(key, must_be_class=nil)
+    prepend_before_filter lambda { load_json_value(params, key, must_be_class) }
+  end
+  accept_param_as_json :reader_tokens, Array
+
   def render_list
     @object_list = {
       :kind  => "arvados##{(@response_resource_name || resource_name).camelize(:lower)}List",