Merged master
authorPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 11 Apr 2014 13:44:13 +0000 (09:44 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 11 Apr 2014 13:44:13 +0000 (09:44 -0400)
34 files changed:
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/models/link.rb
doc/admin/cheat_sheet.html.textile.liquid
doc/api/schema/Link.html.textile.liquid
doc/api/schema/Log.html.textile.liquid
doc/install/create-standard-objects.html.textile.liquid
doc/user/topics/tutorial-trait-search.html.textile.liquid
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/collections_controller.rb
services/api/app/controllers/arvados/v1/jobs_controller.rb
services/api/app/controllers/arvados/v1/keep_disks_controller.rb
services/api/app/controllers/arvados/v1/nodes_controller.rb
services/api/app/controllers/arvados/v1/repositories_controller.rb
services/api/app/controllers/arvados/v1/user_agreements_controller.rb
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/controllers/user_sessions_controller.rb
services/api/app/models/arvados_model.rb
services/api/app/models/collection.rb
services/api/app/models/job.rb
services/api/app/models/keep_disk.rb
services/api/app/models/link.rb
services/api/app/models/log.rb
services/api/app/models/user.rb
services/api/db/migrate/20140325175653_remove_kind_columns.rb [new file with mode: 0644]
services/api/db/schema.rb
services/api/test/fixtures/links.yml
services/api/test/fixtures/virtual_machines.yml
services/api/test/functional/arvados/v1/links_controller_test.rb
services/api/test/integration/permissions_test.rb
services/api/test/integration/valid_links_test.rb [new file with mode: 0644]

index 3ad7944385b82a6f2a7a668334b0cc41107a7411..6afc8c3b040c31cf83375ac7051dc63328851902 100644 (file)
@@ -90,7 +90,6 @@ jQuery(function($){
                            {dataType: 'json',
                             type: $(this).attr('data-remote-method'),
                             data: {
-                                'link[head_kind]': 'arvados#collection',
                                 'link[head_uuid]': tag_head_uuid,
                                 'link[link_class]': 'tag',
                                 'link[name]': new_tag
@@ -130,7 +129,7 @@ jQuery(function($){
             });
         }
     }
-    
+
     var fixer = new HeaderRowFixer('.table-fixed-header-row');
     fixer.duplicateTheadTr();
     fixer.fixThead();
index 74e5831235cf9e881df0df18c3f7ec4c9f1c91ff..8a817f03cd78e52dc61ab7c57bff4dc12ff3fe8a 100644 (file)
@@ -76,9 +76,7 @@ class ActionsController < ApplicationController
 
     chash.each do |k,v|
       l = Link.new({
-                     tail_kind: "arvados#collection",
                      tail_uuid: k,
-                     head_kind: "arvados#collection", 
                      head_uuid: newuuid,
                      link_class: "provenance",
                      name: "provided"
index 01abbb4ee06828f8cf7b529f6bbf77b9bdead716..f24a77ad1fd126b94e2851bebf9aab292a14db6a 100644 (file)
@@ -104,7 +104,7 @@ class CollectionsController < ApplicationController
     Link.where(tail_uuid: @sourcedata.keys).each do |link|
       if link.link_class == 'data_origin'
         @sourcedata[link.tail_uuid][:data_origins] ||= []
-        @sourcedata[link.tail_uuid][:data_origins] << [link.name, link.head_kind, link.head_uuid]
+        @sourcedata[link.tail_uuid][:data_origins] << [link.name, link.head_uuid]
       end
     end
     Collection.where(uuid: @sourcedata.keys).each do |collection|
@@ -112,17 +112,17 @@ class CollectionsController < ApplicationController
         @sourcedata[collection.uuid][:collection] = collection
       end
     end
-    
+
     Collection.where(uuid: @object.uuid).each do |u|
       puts request
-      @prov_svg = ProvenanceHelper::create_provenance_graph(u.provenance, "provenance_svg", 
+      @prov_svg = ProvenanceHelper::create_provenance_graph(u.provenance, "provenance_svg",
                                                             {:request => request,
-                                                              :direction => :bottom_up, 
+                                                              :direction => :bottom_up,
                                                               :combine_jobs => :script_only}) rescue nil
-      @used_by_svg = ProvenanceHelper::create_provenance_graph(u.used_by, "used_by_svg", 
+      @used_by_svg = ProvenanceHelper::create_provenance_graph(u.used_by, "used_by_svg",
                                                                {:request => request,
-                                                                 :direction => :top_down, 
-                                                                 :combine_jobs => :script_only, 
+                                                                 :direction => :top_down,
+                                                                 :combine_jobs => :script_only,
                                                                  :pdata_only => true}) rescue nil
     end
   end
index 5ace8d68193d50cd278d87167fdfaede3194c8c5..0675625f70a6e4df204debca845c18ec02de0781 100644 (file)
@@ -91,9 +91,6 @@ class UsersController < ApplicationController
   def home
     @showallalerts = false
     @my_ssh_keys = AuthorizedKey.where(authorized_user_uuid: current_user.uuid)
-    # @my_vm_perms = Link.where(tail_uuid: current_user.uuid, head_kind: 'arvados#virtual_machine', link_class: 'permission', name: 'can_login')
-    # @my_repo_perms = Link.where(tail_uuid: current_user.uuid, head_kind: 'arvados#repository', link_class: 'permission', name: 'can_write')
-
     @my_tag_links = {}
 
     @my_jobs = Job.
index 3c224aaba0560f492f923e138da7b421c3a17d59..55932213445febe52f3b17587e5fe274e24ea3c0 100644 (file)
@@ -32,9 +32,7 @@ class ArvadosBase < ActiveRecord::Base
       'modified_by_user_uuid' => '004',
       'modified_by_client_uuid' => '005',
       'name' => '050',
-      'tail_kind' => '100',
       'tail_uuid' => '100',
-      'head_kind' => '101',
       'head_uuid' => '101',
       'info' => 'zzz-000',
       'updated_at' => 'zzz-999'
@@ -164,14 +162,12 @@ class ArvadosBase < ActiveRecord::Base
       true
     end
   end
-      
+
   def links(*args)
     o = {}
     o.merge!(args.pop) if args[-1].is_a? Hash
     o[:link_class] ||= args.shift
     o[:name] ||= args.shift
-    o[:head_kind] ||= args.shift
-    o[:tail_kind] = self.kind
     o[:tail_uuid] = self.uuid
     if all_links
       return all_links.select do |m|
index 899a80022ced45b28ce618f2fc847f268550750a..5e7b42a60b0ea985dcda2daf6822d8dcb22c1dec 100644 (file)
@@ -2,6 +2,6 @@ class Link < ArvadosBase
   attr_accessor :head
   attr_accessor :tail
   def self.by_tail(t, opts={})
-    where(opts.merge :tail_kind => t.kind, :tail_uuid => t.uuid)
+    where(opts.merge :tail_uuid => t.uuid)
   end
 end
index daaf012bd19a75c579f6333b359f5176c89e9b89..83fa5e8f1be5f6340f0d01993bca0138ac695891 100644 (file)
@@ -38,9 +38,7 @@ target_username=xxxxxxxchangeme
 
 read -rd $'\000' newlink <<EOF; arv link create --link "$newlink"
 {
-"tail_kind":"arvados#user",
 "tail_uuid":"$user_uuid",
-"head_kind":"arvados#virtualMachine",
 "head_uuid":"$vm_uuid",
 "link_class":"permission",
 "name":"can_login",
@@ -60,9 +58,7 @@ repo_username=xxxxxxxchangeme
 
 read -rd $'\000' newlink <<EOF; arv link create --link "$newlink"
 {
-"tail_kind":"arvados#user",
 "tail_uuid":"$user_uuid",
-"head_kind":"arvados#repository",
 "head_uuid":"$repo_uuid",
 "link_class":"permission",
 "name":"can_write",
index 355605578759982a245360a2f58b27118897e117..dec33bf83272c31f35294b4f1122b5af20fe244b 100644 (file)
@@ -27,11 +27,9 @@ Each link has, in addition to the usual "attributes of Arvados resources":{{site
 table(table table-bordered table-condensed).
 |_. Attribute|_. Type|_. Description|
 |tail_uuid|string|Object UUID at the tail (start, source, origin) of this link|
-|tail_kind|string|Object kind at the tail (start, source, origin) of this link|
 |link_class|string|Class (see below)|
 |name|string|Link type (see below)|
 |head_uuid|string|Object UUID at the head (end, destination, target) of this link|
-|head_kind|string|Object kind at the head (end, destination, target) of this link|
 |properties{}|list|Additional information, expressed as a key&rarr;value hash. Key: string. Value: string, number, array, or hash.|
 
 h2. Link classes
index 3f00339a9c2570853a8083f46d0e3d64fcdde0f2..4d781dc3847b61bc9fcfea5bd32e3b4324842a46 100644 (file)
@@ -30,7 +30,6 @@ Each Log has, in addition to the usual "attributes of Arvados resources":{{site.
 
 table(table table-bordered table-condensed).
 |_. Attribute|_. Type|_. Description|_. Example|
-|object_kind|string|||
 |object_uuid|string|||
 |event_at|datetime|||
 |event_type|string|A user-defined category or type for this event.|@LOGIN@|
index 678127bbfad97fbb0b90e9e7e725746d2042b9c8..b56a503436d1bed7fa8973809706ed2951624898 100644 (file)
@@ -33,9 +33,7 @@ echo "Arvados repository uuid is $repo_uuid"
 
 read -rd $'\000' newlink <<EOF; arv link create --link "$newlink" 
 {
- "tail_kind":"arvados#group",
  "tail_uuid":"$all_users_group_uuid",
- "head_kind":"arvados#repository",
  "head_uuid":"$repo_uuid",
  "link_class":"permission",
  "name":"can_read" 
index 001fbbc08267fe00e9c5508b661134895657a544..a79495e377e415c88088eb1cc65d0777086415b1 100644 (file)
@@ -67,22 +67,22 @@ h2. Finding humans with the selected trait
 We query the "links" resource to find humans that report the selected trait.  Links are directional connections between Arvados data items, for example, from a human to their reported traits.
 
 <notextile>
-<pre><code>&gt;&gt;&gt; <span class="userinput">trait_query = {
-    'link_class': 'human_trait',
-    'tail_kind': 'arvados#human',
-    'head_uuid': non_melanoma_cancer
-  }
+<pre><code>&gt;&gt;&gt; <span class="userinput">trait_filter = [
+    ['link_class', '=', 'human_trait'],
+    ['tail_uuid', 'is_a', 'arvados#human'],
+    ['head_uuid', '=', non_melanoma_cancer],
+  ]
 </code></pre>
 </notextile>
 
-* @'link_class'@ queries for links that describe the traits of a particular human.
-* @'tail_kind'@ queries for links where the tail of the link is a human.
-* @'head_uuit'@ queries for links where the head of the link is a specific data item.
+* @['link_class', '=', 'human_trait']@ filters on links that connect phenotype traits to individuals in the database.
+* @['tail_uuid', 'is_a', 'arvados#human']@ filters that the "tail" must be a "human" database object.
+* @['head_uuid', '=', non_melanoma_cancer]@ filters that the "head" of the link must connect to the "trait" database object non_melanoma_cancer .
 
 The query will return links that match all three conditions.
 
 <notextile>
-<pre><code>&gt;&gt;&gt; <span class="userinput">trait_links = arvados.api().links().list(limit=1000, where=trait_query).execute()</span>
+<pre><code>&gt;&gt;&gt; <span class="userinput">trait_links = arvados.api().links().list(limit=1000, filters=trait_filter).execute()</span>
 </code></pre>
 </notextile>
 
index 06e1838411b4a4d4171bf2884f323d61a9737ebf..94ef19c87948fbf4225ec065d5b4c2072ddb6e22 100644 (file)
@@ -36,22 +36,16 @@ class ApplicationController < ActionController::Base
 
   def create
     @object = model_class.new resource_attrs
-    if @object.save
-      show
-    else
-      raise "Save failed"
-    end
+    @object.save!
+    show
   end
 
   def update
     attrs_to_update = resource_attrs.reject { |k,v|
       [:kind, :etag, :href].index k
     }
-    if @object.update_attributes attrs_to_update
-      show
-    else
-      raise "Update failed"
-    end
+    @object.update_attributes! attrs_to_update
+    show
   end
 
   def destroy
@@ -166,6 +160,19 @@ class ApplicationController < ActionController::Base
             cond_out << "#{table_name}.#{attr} IN (?)"
             param_out << operand
           end
+        when 'is_a'
+          operand = [operand] unless operand.is_a? Array
+          cond = []
+          operand.each do |op|
+              cl = ArvadosModel::kind_class op
+              if cl
+                cond << "#{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?
index c0cd419819124f34f4de532e641fa013b1907c90..8db93c36c2171fa310e6939ae00ddd830dd06ee7 100644 (file)
@@ -6,11 +6,6 @@ class Arvados::V1::CollectionsController < ApplicationController
     # exist) giving the current user (or specified owner_uuid)
     # permission to read it.
     owner_uuid = resource_attrs.delete(:owner_uuid) || current_user.uuid
-    owner_kind = if owner_uuid.match(/-(\w+)-/)[1] == User.uuid_prefix
-                   'arvados#user'
-                 else
-                   'arvados#group'
-                 end
     unless current_user.can? write: owner_uuid
       logger.warn "User #{current_user.andand.uuid} tried to set collection owner_uuid to #{owner_uuid}"
       raise ArvadosModel::PermissionDeniedError
@@ -36,9 +31,7 @@ class Arvados::V1::CollectionsController < ApplicationController
           owner_uuid: owner_uuid,
           link_class: 'permission',
           name: 'can_read',
-          head_kind: 'arvados#collection',
           head_uuid: @object.uuid,
-          tail_kind: owner_kind,
           tail_uuid: owner_uuid
         }
         ActiveRecord::Base.transaction do
index 178b48f173d58e47b5c590149d8b7f966b872dec..40f2def5dcbfd3a7546e55e68ebfcdc3547f4961 100644 (file)
@@ -57,7 +57,7 @@ class Arvados::V1::JobsController < ApplicationController
 
   def cancel
     reload_object_before_update
-    @object.update_attributes cancelled_at: Time.now
+    @object.update_attributes! cancelled_at: Time.now
     show
   end
 
index 7db295dbb2250be51f524969227bd3b7af086fc7..3d9191641ee5a1d0f92a65b6e76cb7cd7b99c86a 100644 (file)
@@ -12,15 +12,18 @@ class Arvados::V1::KeepDisksController < ApplicationController
       service_ssl_flag: true
     }
   end
+
   def ping
     params[:service_host] ||= request.env['REMOTE_ADDR']
-    if not @object.ping params
-      return render_not_found "object not found"
+    act_as_system_user do
+      if not @object.ping params
+        return render_not_found "object not found"
+      end
+      # Render the :superuser view (i.e., include the ping_secret) even
+      # if !current_user.is_admin. This is safe because @object.ping's
+      # success implies the ping_secret was already known by the client.
+      render json: @object.as_api_response(:superuser)
     end
-    # Render the :superuser view (i.e., include the ping_secret) even
-    # if !current_user.is_admin. This is safe because @object.ping's
-    # success implies the ping_secret was already known by the client.
-    render json: @object.as_api_response(:superuser)
   end
 
   def find_objects_for_index
index 1461eeccaa1481fc568eb2a0a8d91a8be8b18562..4415a511631df1b4646b974c7595816b9f9db71d 100644 (file)
@@ -13,18 +13,21 @@ class Arvados::V1::NodesController < ApplicationController
   def self._ping_requires_parameters
     { ping_secret: true }
   end
+
   def ping
-    @object = Node.where(uuid: (params[:id] || params[:uuid])).first
-    if !@object
-      return render_not_found
-    end
-    @object.ping({ ip: params[:local_ipv4] || request.env['REMOTE_ADDR'],
-                   ping_secret: params[:ping_secret],
-                   ec2_instance_id: params[:instance_id] })
-    if @object.info[:ping_secret] == params[:ping_secret]
-      render json: @object.as_api_response(:superuser)
-    else
-      raise "Invalid ping_secret after ping"
+    act_as_system_user do 
+      @object = Node.where(uuid: (params[:id] || params[:uuid])).first
+      if !@object
+        return render_not_found
+      end
+      @object.ping({ ip: params[:local_ipv4] || request.env['REMOTE_ADDR'],
+                     ping_secret: params[:ping_secret],
+                     ec2_instance_id: params[:instance_id] })
+      if @object.info[:ping_secret] == params[:ping_secret]
+        render json: @object.as_api_response(:superuser)
+      else
+        raise "Invalid ping_secret after ping"
+      end
     end
   end
 
index 19504e10c8d83d6e5a06a20bfd8a57e17e556e28..390aa73324fd4a3eba0b56a245819b587f26d9f9 100644 (file)
@@ -14,7 +14,7 @@ class Arvados::V1::RepositoriesController < ApplicationController
       gitolite_permissions = ''
       perms = []
       repo.permissions.each do |perm|
-        if perm.tail_kind == 'arvados#group'
+        if ArvadosModel::resource_class_for_uuid(perm.tail_uuid) == Group
           @users.each do |user_uuid, user|
             user.group_permissions.each do |group_uuid, perm_mask|
               if perm_mask[:write]
index 4ad959e86aae9079554c9f0e24d77c840dea7482..32adde9507554ee9195bbc812b51cc1d86d753ba 100644 (file)
@@ -19,12 +19,12 @@ class Arvados::V1::UserAgreementsController < ApplicationController
     else
       current_user_uuid = current_user.uuid
       act_as_system_user do
-        uuids = Link.where(owner_uuid: system_user_uuid,
-                           link_class: 'signature',
-                           name: 'require',
-                           tail_kind: 'arvados#user',
-                           tail_uuid: system_user_uuid,
-                           head_kind: 'arvados#collection').
+        uuids = Link.where("owner_uuid = ? and link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
+                           system_user_uuid,
+                           'signature',
+                           'require',
+                           system_user_uuid,
+                           Collection.uuid_like_pattern).
           collect &:head_uuid
         @objects = Collection.where('uuid in (?)', uuids)
       end
@@ -37,12 +37,12 @@ class Arvados::V1::UserAgreementsController < ApplicationController
     current_user_uuid = (current_user.andand.is_admin && params[:uuid]) ||
       current_user.uuid
     act_as_system_user do
-      @objects = Link.where(owner_uuid: system_user_uuid,
-                            link_class: 'signature',
-                            name: 'click',
-                            tail_kind: 'arvados#user',
-                            tail_uuid: current_user_uuid,
-                            head_kind: 'arvados#collection')
+      @objects = Link.where("owner_uuid = ? and link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
+                            system_user_uuid,
+                            'signature',
+                            'click',
+                            current_user_uuid,
+                            Collection.uuid_like_pattern)
     end
     @response_resource_name = 'link'
     render_list
@@ -53,9 +53,7 @@ class Arvados::V1::UserAgreementsController < ApplicationController
     act_as_system_user do
       @object = Link.create(link_class: 'signature',
                             name: 'click',
-                            tail_kind: 'arvados#user',
                             tail_uuid: current_user_uuid,
-                            head_kind: 'arvados#collection',
                             head_uuid: params[:uuid])
     end
     show
index 58661a0e9a3c970712eccc054b8d2e9060dc249e..0934642261bb314fdfbf76175251a2d702fb3a6b 100644 (file)
@@ -59,19 +59,18 @@ class Arvados::V1::UsersController < ApplicationController
           "but is not invited"
         raise ArgumentError.new "Cannot activate without being invited."
       end
-      act_as_system_user do
-        required_uuids = Link.where(owner_uuid: system_user_uuid,
-                                    link_class: 'signature',
-                                    name: 'require',
-                                    tail_uuid: system_user_uuid,
-                                    head_kind: 'arvados#collection').
+      act_as_system_user do       
+        required_uuids = Link.where("owner_uuid = ? and link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
+                                    system_user_uuid,
+                                    'signature',
+                                    'require',
+                                    system_user_uuid,
+                                    Collection.uuid_like_pattern).
           collect(&:head_uuid)
         signed_uuids = Link.where(owner_uuid: system_user_uuid,
                                   link_class: 'signature',
                                   name: 'click',
-                                  tail_kind: 'arvados#user',
                                   tail_uuid: @object.uuid,
-                                  head_kind: 'arvados#collection',
                                   head_uuid: required_uuids).
           collect(&:head_uuid)
         todo_uuids = required_uuids - signed_uuids
index 3674c010cb7bcd97ae808483997ef5118554042f..a7391bd73266a2b0b52decec09fa48057f62db2d 100644 (file)
@@ -24,11 +24,11 @@ class UserSessionsController < ApplicationController
     if not user
       # Check for permission to log in to an existing User record with
       # a different identity_url
-      Link.where(link_class: 'permission',
-                 name: 'can_login',
-                 tail_kind: 'email',
-                 tail_uuid: omniauth['info']['email'],
-                 head_kind: 'arvados#user').each do |link|
+      Link.where("link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
+                 'permission',
+                 'can_login',
+                 omniauth['info']['email'],
+                 User.uuid_like_pattern).each do |link|
         if prefix = link.properties['identity_url_prefix']
           if prefix == omniauth['info']['identity_url'][0..prefix.size-1]
             user = User.find_by_uuid(link.head_uuid)
index 38c7a797d164d0a3b984246d89ae6a2f91263aa7..493cf828d165abaf6f6163daae6d714d0e8b8c8f 100644 (file)
@@ -11,10 +11,11 @@ class ArvadosModel < ActiveRecord::Base
   before_create :ensure_permission_to_create
   before_update :ensure_permission_to_update
   before_destroy :ensure_permission_to_destroy
-  before_create :update_modified_by_fields
-  before_update :maybe_update_modified_by_fields
+
+  before_validation :maybe_update_modified_by_fields
   validate :ensure_serialized_attribute_type
   validate :normalize_collection_uuids
+  validate :ensure_valid_uuids
 
   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
 
@@ -31,7 +32,7 @@ class ArvadosModel < ActiveRecord::Base
   end
 
   def self.kind_class(kind)
-    kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
+    kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
   end
 
   def href
@@ -55,18 +56,18 @@ class ArvadosModel < ActiveRecord::Base
     self.columns.select { |col| col.name == attr.to_s }.first
   end
 
-  def eager_load_associations
-    self.class.columns.each do |col|
-      re = col.name.match /^(.*)_kind$/
-      if (re and
-          self.respond_to? re[1].to_sym and
-          (auuid = self.send((re[1] + '_uuid').to_sym)) and
-          (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
-          (aobject = aclass.where('uuid=?', auuid).first))
-        self.instance_variable_set('@'+re[1], aobject)
-      end
-    end
-  end
+  def eager_load_associations
+    self.class.columns.each do |col|
+      re = col.name.match /^(.*)_kind$/
+      if (re and
+          self.respond_to? re[1].to_sym and
+          (auuid = self.send((re[1] + '_uuid').to_sym)) and
+          (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
+          (aobject = aclass.where('uuid=?', auuid).first))
+        self.instance_variable_set('@'+re[1], aobject)
+      end
+    end
+  end
 
   def self.readable_by user
     uuid_list = [user.uuid, *user.groups_i_can(:read)]
@@ -140,7 +141,7 @@ class ArvadosModel < ActiveRecord::Base
   end
 
   def maybe_update_modified_by_fields
-    update_modified_by_fields if self.changed?
+    update_modified_by_fields if self.changed? or self.new_record?
   end
 
   def update_modified_by_fields
@@ -171,6 +172,10 @@ class ArvadosModel < ActiveRecord::Base
     attributes.keys.select { |a| a.match /_uuid$/ }
   end
 
+  def skip_uuid_read_permission_check
+    %w(modified_by_client_uuid)
+  end
+
   def normalize_collection_uuids
     foreign_key_attributes.each do |attr|
       attr_value = send attr
@@ -185,6 +190,45 @@ class ArvadosModel < ActiveRecord::Base
     end
   end
 
+  @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
+
+  @@prefixes_hash = nil
+  def self.uuid_prefixes
+    unless @@prefixes_hash
+      @@prefixes_hash = {}
+      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
+        if k.respond_to?(:uuid_prefix)
+          @@prefixes_hash[k.uuid_prefix] = k
+        end
+      end
+    end
+    @@prefixes_hash
+  end
+
+  def self.uuid_like_pattern
+    "_____-#{uuid_prefix}-_______________"
+  end
+
+  def ensure_valid_uuids
+    specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
+
+    foreign_key_attributes.each do |attr|
+      begin
+        if new_record? or send (attr + "_changed?")
+          attr_value = send attr
+          r = ArvadosModel::resource_class_for_uuid attr_value if attr_value
+          r = r.readable_by(current_user) if r and not skip_uuid_read_permission_check.include? attr
+          if r and r.where(uuid: attr_value).count == 0 and not specials.include? attr_value
+            errors.add(attr, "'#{attr_value}' not found")
+          end
+        end
+      rescue Exception => e
+        bt = e.backtrace.join("\n")
+        errors.add(attr, "'#{attr_value}' error '#{e}'\n#{bt}\n")
+      end
+    end
+  end
+
   def self.resource_class_for_uuid(uuid)
     if uuid.is_a? ArvadosModel
       return uuid.class
@@ -198,14 +242,8 @@ class ArvadosModel < ActiveRecord::Base
     resource_class = nil
 
     Rails.application.eager_load!
-    uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
-      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
-        if k.respond_to?(:uuid_prefix)
-          if k.uuid_prefix == re[1]
-            return k
-          end
-        end
-      end
+    uuid.match @@UUID_REGEX do |re|
+      return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
     end
     nil
   end
index 620b74a96ab90c6fce71a16864b2160dcc71558e..600c07511b67e249cb0ae312ebbf9ad5e5c8b9cb 100644 (file)
@@ -128,6 +128,10 @@ class Collection < ArvadosModel
     end
   end
 
+  def self.uuid_like_pattern
+    "________________________________+%"
+  end
+
   def self.normalize_uuid uuid
     hash_part = nil
     size_part = nil
index e3881ba6e1f4bfdfc492975eac82bcccaeaba1e4..0b2247bc21d99196b839f58a0dca37a2d10c284f 100644 (file)
@@ -70,6 +70,10 @@ class Job < ArvadosModel
     super + %w(output log)
   end
 
+  def skip_uuid_read_permission_check
+    super + %w(cancelled_by_client_uuid)
+  end
+
   def ensure_script_version_is_commit
     if self.is_locked_by_uuid and self.started_at
       # Apparently client has already decided to go for it. This is
index 0998fcd84a1b5f9ba855aebbcf4440426f598487..77fc6278eba531f6baa1acf997044aaf893121c6 100644 (file)
@@ -22,6 +22,10 @@ class KeepDisk < ArvadosModel
     t.add :ping_secret
   end
 
+  def foreign_key_attributes
+    super.reject { |a| a == "filesystem_uuid" }
+  end
+
   def ping(o)
     raise "must have :service_host and :ping_secret" unless o[:service_host] and o[:ping_secret]
 
@@ -31,7 +35,7 @@ class KeepDisk < ArvadosModel
     end
 
     @bypass_arvados_authorization = true
-    self.update_attributes(o.select { |k,v|
+    self.update_attributes!(o.select { |k,v|
                              [:service_host,
                               :service_port,
                               :service_ssl_flag,
index 1d4e13d18618eddb14a98aac80aa60f591b5c58a..8e17ce697e52c16e1553b6a505297c1aa51ade6a 100644 (file)
@@ -13,11 +13,9 @@ class Link < ArvadosModel
   attr_accessor :tail
 
   api_accessible :user, extend: :common do |t|
-    t.add :tail_kind
     t.add :tail_uuid
     t.add :link_class
     t.add :name
-    t.add :head_kind
     t.add :head_uuid
     t.add :head, :if => :head
     t.add :tail, :if => :tail
@@ -43,7 +41,7 @@ class Link < ArvadosModel
 
     # All users can grant permissions on objects they own
     head_obj = self.class.
-      kind_class(self.head_kind).
+      kind_class(self.head_uuid).
       where('uuid=?',head_uuid).
       first
     if head_obj
index 29efc9dc1136427e471d63ce3aedd0caaf58d193..bf3c8d3eb43c537ad4fc1cdd5732b4db648c2e64 100644 (file)
@@ -7,7 +7,6 @@ class Log < ArvadosModel
   attr_accessor :object
 
   api_accessible :user, extend: :common do |t|
-    t.add :object_kind
     t.add :object_uuid
     t.add :object, :if => :object
     t.add :event_at
index 5c03eda519533d839abfca77b07b80f6342bfc2c..41858e8b3adb3cb2394c0eb55d5a3cfc24a1d42e 100644 (file)
@@ -80,10 +80,11 @@ class User < ArvadosModel
         Group.where('owner_uuid in (?)', lookup_uuids).each do |group|
           newgroups << [group.owner_uuid, group.uuid, 'can_manage']
         end
-        Link.where('tail_uuid in (?) and link_class = ? and head_kind in (?)',
+        Link.where('tail_uuid in (?) and link_class = ? and (head_uuid like ? or head_uuid like ?)',
                    lookup_uuids,
                    'permission',
-                   ['arvados#group', 'arvados#user']).each do |link|
+                   Group.uuid_like_pattern,
+                   User.uuid_like_pattern).each do |link|
           newgroups << [link.tail_uuid, link.head_uuid, link.name]
         end
         newgroups.each do |tail_uuid, head_uuid, perm_name|
diff --git a/services/api/db/migrate/20140325175653_remove_kind_columns.rb b/services/api/db/migrate/20140325175653_remove_kind_columns.rb
new file mode 100644 (file)
index 0000000..115048d
--- /dev/null
@@ -0,0 +1,13 @@
+class RemoveKindColumns < ActiveRecord::Migration
+  def up
+    remove_column :links, :head_kind
+    remove_column :links, :tail_kind
+    remove_column :logs, :object_kind
+  end
+
+  def down
+    add_column :links, :head_kind, :string
+    add_column :links, :tail_kind, :string
+    add_column :logs, :object_kind, :string
+  end
+end
index fe31666694fee6a112b9c0f3af3febb92d3b9ceb..a63fdf4fe7a75cf2dad81bfa256ca68ae29b470a 100644 (file)
@@ -240,20 +240,16 @@ ActiveRecord::Schema.define(:version => 20140402001908) do
     t.string   "modified_by_user_uuid"
     t.datetime "modified_at"
     t.string   "tail_uuid"
-    t.string   "tail_kind"
     t.string   "link_class"
     t.string   "name"
     t.string   "head_uuid"
     t.text     "properties"
     t.datetime "updated_at"
-    t.string   "head_kind"
   end
 
   add_index "links", ["created_at"], :name => "index_links_on_created_at"
-  add_index "links", ["head_kind"], :name => "index_links_on_head_kind"
   add_index "links", ["head_uuid"], :name => "index_links_on_head_uuid"
   add_index "links", ["modified_at"], :name => "index_links_on_modified_at"
-  add_index "links", ["tail_kind"], :name => "index_links_on_tail_kind"
   add_index "links", ["tail_uuid"], :name => "index_links_on_tail_uuid"
   add_index "links", ["uuid"], :name => "index_links_on_uuid", :unique => true
 
@@ -262,7 +258,6 @@ ActiveRecord::Schema.define(:version => 20140402001908) do
     t.string   "owner_uuid"
     t.string   "modified_by_client_uuid"
     t.string   "modified_by_user_uuid"
-    t.string   "object_kind"
     t.string   "object_uuid"
     t.datetime "event_at"
     t.string   "event_type"
@@ -277,7 +272,6 @@ ActiveRecord::Schema.define(:version => 20140402001908) do
   add_index "logs", ["event_at"], :name => "index_logs_on_event_at"
   add_index "logs", ["event_type"], :name => "index_logs_on_event_type"
   add_index "logs", ["modified_at"], :name => "index_logs_on_modified_at"
-  add_index "logs", ["object_kind"], :name => "index_logs_on_object_kind"
   add_index "logs", ["object_uuid"], :name => "index_logs_on_object_uuid"
   add_index "logs", ["summary"], :name => "index_logs_on_summary"
   add_index "logs", ["uuid"], :name => "index_logs_on_uuid", :unique => true
index 0342d3d1aa654219c5315890ee688c7158de8b84..47de7e5abc7963333c5a7d42c63dbb9bab19edfd 100644 (file)
@@ -6,11 +6,9 @@ user_agreement_required:
   modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
   modified_at: 2013-12-26T19:52:21Z
   updated_at: 2013-12-26T19:52:21Z
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-000000000000000
   link_class: signature
   name: require
-  head_kind: arvados#collection
   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
   properties: {}
 
@@ -22,11 +20,9 @@ user_agreement_readable:
   modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#group
   tail_uuid: zzzzz-j7d0g-fffffffffffffff
   link_class: permission
   name: can_read
-  head_kind: arvados#collection
   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
   properties: {}
 
@@ -38,11 +34,9 @@ active_user_member_of_all_users_group:
   modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   link_class: permission
   name: can_read
-  head_kind: arvados#group
   head_uuid: zzzzz-j7d0g-fffffffffffffff
   properties: {}
 
@@ -54,11 +48,9 @@ active_user_can_manage_system_owned_group:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-02-03 15:42:26 -0800
   updated_at: 2014-02-03 15:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   link_class: permission
   name: can_manage
-  head_kind: arvados#group
   head_uuid: zzzzz-j7d0g-8ulrifv67tve5sx
   properties: {}
 
@@ -70,11 +62,9 @@ user_agreement_signed_by_active:
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   modified_at: 2013-12-26T20:52:21Z
   updated_at: 2013-12-26T20:52:21Z
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   link_class: signature
   name: click
-  head_kind: arvados#collection
   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
   properties: {}
 
@@ -86,11 +76,9 @@ user_agreement_signed_by_inactive:
   modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
   modified_at: 2013-12-26T20:52:21Z
   updated_at: 2013-12-26T20:52:21Z
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-7sg468ezxwnodxs
   link_class: signature
   name: click
-  head_kind: arvados#collection
   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
   properties: {}
 
@@ -102,11 +90,9 @@ spectator_user_member_of_all_users_group:
   modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
   link_class: permission
   name: can_read
-  head_kind: arvados#group
   head_uuid: zzzzz-j7d0g-fffffffffffffff
   properties: {}
 
@@ -118,11 +104,9 @@ inactive_user_member_of_all_users_group:
   modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
   modified_at: 2013-12-26T20:52:21Z
   updated_at: 2013-12-26T20:52:21Z
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-x9kqpd79egh49c7
   link_class: permission
   name: can_read
-  head_kind: arvados#group
   head_uuid: zzzzz-j7d0g-fffffffffffffff
   properties: {}
 
@@ -134,11 +118,9 @@ inactive_signed_ua_user_member_of_all_users_group:
   modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
   modified_at: 2013-12-26T20:52:21Z
   updated_at: 2013-12-26T20:52:21Z
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-7sg468ezxwnodxs
   link_class: permission
   name: can_read
-  head_kind: arvados#group
   head_uuid: zzzzz-j7d0g-fffffffffffffff
   properties: {}
 
@@ -150,11 +132,9 @@ foo_file_readable_by_active:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   link_class: permission
   name: can_read
-  head_kind: arvados#collection
   head_uuid: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   properties: {}
 
@@ -198,11 +178,9 @@ bar_file_readable_by_active:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   link_class: permission
   name: can_read
-  head_kind: arvados#collection
   head_uuid: fa7aeb5140e2848d39b416daeef4ffc5+45
   properties: {}
 
@@ -214,11 +192,9 @@ bar_file_readable_by_spectator:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
   link_class: permission
   name: can_read
-  head_kind: arvados#collection
   head_uuid: fa7aeb5140e2848d39b416daeef4ffc5+45
   properties: {}
 
@@ -230,11 +206,9 @@ baz_file_publicly_readable:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#group
   tail_uuid: zzzzz-j7d0g-fffffffffffffff
   link_class: permission
   name: can_read
-  head_kind: arvados#collection
   head_uuid: ea10d51bcf88862dbcc36eb292017dfd+45
   properties: {}
 
@@ -246,11 +220,9 @@ barbaz_job_readable_by_spectator:
   modified_by_user_uuid: zzzzz-tpzed-000000000000000
   modified_at: 2014-01-24 20:42:26 -0800
   updated_at: 2014-01-24 20:42:26 -0800
-  tail_kind: arvados#user
   tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
   link_class: permission
   name: can_read
-  head_kind: arvados#job
   head_uuid: zzzzz-8i9sb-cjs4pklxxjykyuq
   properties: {}
 
index 72e2130563a48bc7c5a55db9114a3d14b1927932..f5e01639b6c5c97bfef133731e2d31dbe8fed696 100644 (file)
@@ -2,3 +2,8 @@ testvm:
   uuid: zzzzz-2x53u-382brsig8rp3064
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
   hostname: testvm.shell
+
+testvm2:
+  uuid: zzzzz-2x53u-382brsig8rp3065
+  owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  hostname: testvm2.shell
index afecc18bb01d778f536cc9dbdf6ee5bb6799d3f9..d2863f37ecd9ddb390a0cc72f5805386e74da9a5 100644 (file)
@@ -7,9 +7,7 @@ class Arvados::V1::LinksControllerTest < ActionController::TestCase
       properties: {username: 'testusername'},
       link_class: 'test',
       name: 'encoding',
-      tail_kind: 'arvados#user',
       tail_uuid: users(:admin).uuid,
-      head_kind: 'arvados#virtualMachine',
       head_uuid: virtual_machines(:testvm).uuid
     }
     authorize_with :admin
@@ -22,4 +20,86 @@ class Arvados::V1::LinksControllerTest < ActionController::TestCase
     end
   end
   
+  test "head must exist" do
+    link = {
+      link_class: 'test',
+      name: 'stuff',
+      tail_uuid: users(:active).uuid,
+      head_uuid: 'zzzzz-tpzed-xyzxyzxerrrorxx'
+    }
+    authorize_with :admin
+    post :create, link: link
+    assert_response 422
+  end
+
+  test "tail must exist" do
+    link = {
+      link_class: 'test',
+      name: 'stuff',
+      head_uuid: users(:active).uuid,
+      tail_uuid: 'zzzzz-tpzed-xyzxyzxerrrorxx'
+    }
+    authorize_with :admin
+    post :create, link: link
+    assert_response 422
+  end
+
+  test "tail must be visible by user" do
+    link = {
+      link_class: 'test',
+      name: 'stuff',
+      head_uuid: users(:active).uuid,
+      tail_uuid: virtual_machines(:testvm).uuid
+    }
+    authorize_with :active
+    post :create, link: link
+    assert_response 422
+  end
+
+  test "filter links with 'is_a' operator" do
+    authorize_with :admin
+    get :index, {
+      filters: [ ['tail_uuid', 'is_a', 'arvados#user'] ]
+    }
+    assert_response :success
+    found = assigns(:objects)
+    assert_not_equal 0, found.count
+    assert_equal found.count, (found.select { |f| f.tail_uuid.match /[a-z0-9]{5}-tpzed-[a-z0-9]{15}/}).count
+  end
+
+  test "filter links with 'is_a' operator with more than one" do
+    authorize_with :admin
+    get :index, {
+      filters: [ ['tail_uuid', 'is_a', ['arvados#user', 'arvados#group'] ] ],
+    }
+    assert_response :success
+    found = assigns(:objects)
+    assert_not_equal 0, found.count
+    assert_equal found.count, (found.select { |f| f.tail_uuid.match /[a-z0-9]{5}-(tpzed|j7d0g)-[a-z0-9]{15}/}).count
+  end
+
+  test "filter links with 'is_a' operator with bogus type" do
+    authorize_with :admin
+    get :index, {
+      filters: [ ['tail_uuid', 'is_a', ['arvados#bogus'] ] ],
+    }
+    assert_response :success
+    found = assigns(:objects)
+    assert_equal 0, found.count
+  end
+
+  test "filter links with 'is_a' operator with collection" do
+    authorize_with :admin
+    get :index, {
+      filters: [ ['head_uuid', 'is_a', ['arvados#collection'] ] ],
+    }
+    assert_response :success
+    found = assigns(:objects)
+    assert_response :success
+    found = assigns(:objects)
+    assert_not_equal 0, found.count
+    assert_equal found.count, (found.select { |f| f.head_uuid.match /[a-f0-9]{32}\+\d+/}).count
+  end
+
+
 end
index 40a77e72c56b3c814ec28c098b7501a319667bb9..9a190aab952f3a6b164f9d9d4ec6ef378b13a895 100644 (file)
@@ -16,11 +16,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#user',
         tail_uuid: users(:spectator).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#collection',
         head_uuid: collections(:foo_file).uuid,
         properties: {}
       }
@@ -31,11 +29,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#user',
         tail_uuid: users(:spectator).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#collection',
         head_uuid: collections(:foo_file).uuid,
         properties: {}
       }
@@ -70,11 +66,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#user',
         tail_uuid: users(:spectator).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#group',
         head_uuid: groups(:private).uuid,
         properties: {}
       }
@@ -89,11 +83,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#group',
         tail_uuid: groups(:private).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#collection',
         head_uuid: collections(:foo_file).uuid,
         properties: {}
       }
@@ -125,11 +117,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#group',
         tail_uuid: groups(:private).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#collection',
         head_uuid: collections(:foo_file).uuid,
         properties: {}
       }
@@ -144,11 +134,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#user',
         tail_uuid: users(:spectator).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#group',
         head_uuid: groups(:private).uuid,
         properties: {}
       }
@@ -179,11 +167,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#user',
         tail_uuid: users(:spectator).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#group',
         head_uuid: groups(:private).uuid,
         properties: {}
       }
@@ -194,11 +180,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#group',
         tail_uuid: groups(:private).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#group',
         head_uuid: groups(:empty_lonely_group).uuid,
         properties: {}
       }
@@ -209,11 +193,9 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     post "/arvados/v1/links", {
       :format => :json,
       :link => {
-        tail_kind: 'arvados#group',
         tail_uuid: groups(:empty_lonely_group).uuid,
         link_class: 'permission',
         name: 'can_read',
-        head_kind: 'arvados#collection',
         head_uuid: collections(:foo_file).uuid,
         properties: {}
       }
diff --git a/services/api/test/integration/valid_links_test.rb b/services/api/test/integration/valid_links_test.rb
new file mode 100644 (file)
index 0000000..65431f3
--- /dev/null
@@ -0,0 +1,42 @@
+require 'test_helper'
+
+class ValidLinksTest < ActionDispatch::IntegrationTest
+  fixtures :all
+
+  test "tail must exist on update" do
+    admin_auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin).api_token}"}
+
+    post "/arvados/v1/links", {
+      :format => :json,
+      :link => {
+        link_class: 'test',
+        name: 'stuff',
+        head_uuid: users(:active).uuid,
+        tail_uuid: virtual_machines(:testvm).uuid
+      }
+    }, admin_auth
+    assert_response :success
+    u = jresponse['uuid']
+
+    put "/arvados/v1/links/#{u}", {
+      :format => :json,
+      :link => {
+        tail_uuid: virtual_machines(:testvm2).uuid
+      }
+    }, admin_auth
+    assert_response :success
+    #puts @response.body
+    #puts jresponse['tail_uuid']
+    #puts virtual_machines(:testvm2)
+    assert_equal virtual_machines(:testvm2).uuid, (ActiveSupport::JSON.decode @response.body)['tail_uuid']
+
+    put "/arvados/v1/links/#{u}", {
+      :format => :json,
+      :link => {
+        tail_uuid: 'zzzzz-tpzed-xyzxyzxerrrorxx'
+      }
+    }, admin_auth
+    assert_response 422
+  end
+
+end