Prevent ownership cycles.
authorTom Clegg <tom@curoverse.com>
Sun, 4 May 2014 01:08:18 +0000 (21:08 -0400)
committerTom Clegg <tom@curoverse.com>
Sun, 4 May 2014 01:08:18 +0000 (21:08 -0400)
services/api/app/models/arvados_model.rb
services/api/app/models/node.rb
services/api/lib/current_api_client.rb
services/api/test/fixtures/api_clients.yml
services/api/test/fixtures/groups.yml
services/api/test/fixtures/links.yml
services/api/test/test_helper.rb
services/api/test/unit/group_test.rb
services/api/test/unit/log_test.rb

index 4f06f054a3bb215b3b80b8365ea383ae6911755f..0b577d1d47d84ce600ba4fe6b2a988b3e7870573 100644 (file)
@@ -9,8 +9,10 @@ class ArvadosModel < ActiveRecord::Base
   attr_protected :modified_by_client_uuid
   attr_protected :modified_at
   after_initialize :log_start_state
-  before_create :ensure_permission_to_create
-  before_update :ensure_permission_to_update
+  before_save :ensure_permission_to_save
+  before_save :ensure_owner_uuid_is_permitted
+  before_save :ensure_ownership_path_leads_to_user
+  before_destroy :ensure_owner_uuid_is_permitted
   before_destroy :ensure_permission_to_destroy
 
   before_create :update_modified_by_fields
@@ -130,16 +132,69 @@ class ArvadosModel < ActiveRecord::Base
 
   protected
 
-  def ensure_permission_to_create
-    raise PermissionDeniedError unless permission_to_create
+  def ensure_ownership_path_leads_to_user
+    if new_record? or owner_uuid_changed?
+      uuid_in_path = {owner_uuid => true, uuid => true}
+      x = owner_uuid
+      while (owner_class = self.class.resource_class_for_uuid(x)) != User
+        begin
+          if x == uuid
+            # Test for cycles with the new version, not the DB contents
+            x = owner_uuid
+          else
+            x = owner_class.find_by_uuid(x).owner_uuid
+          end
+        rescue ActiveRecord::RecordNotFound => e
+          errors.add :owner_uuid, "is not owned by any user: #{e}"
+          return false
+        end
+        if uuid_in_path[x]
+          if x == owner_uuid
+            errors.add :owner_uuid, "would create an ownership cycle"
+          else
+            errors.add :owner_uuid, "has an ownership cycle"
+          end
+          return false
+        end
+        uuid_in_path[x] = true
+      end
+    end
+    true
   end
 
-  def permission_to_create
-    current_user.andand.is_active
+  def ensure_owner_uuid_is_permitted
+    return false if !current_user
+    self.owner_uuid ||= current_user.uuid
+    if self.owner_uuid_changed?
+      if current_user.uuid == self.owner_uuid or
+          current_user.can? write: self.owner_uuid
+        # current_user is, or has :write permission on, the new owner
+      else
+        logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
+        return false
+      end
+    end
+    if new_record?
+      return true
+    elsif current_user.uuid == self.owner_uuid_was or
+        current_user.uuid == self.uuid or
+        current_user.can? write: self.owner_uuid_was
+      # current user is, or has :write permission on, the previous owner
+      return true
+    else
+      logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
+      raise PermissionDeniedError
+    end
+  end
+
+  def ensure_permission_to_save
+    unless (new_record? ? permission_to_create : permission_to_update)
+      raise PermissionDeniedError
+    end
   end
 
-  def ensure_permission_to_update
-    raise PermissionDeniedError unless permission_to_update
+  def permission_to_create
+    current_user.andand.is_active
   end
 
   def permission_to_update
@@ -156,24 +211,7 @@ class ArvadosModel < ActiveRecord::Base
       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
       return false
     end
-    if self.owner_uuid_changed?
-      if current_user.uuid == self.owner_uuid or
-          current_user.can? write: self.owner_uuid
-        # current_user is, or has :write permission on, the new owner
-      else
-        logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
-        return false
-      end
-    end
-    if current_user.uuid == self.owner_uuid_was or
-        current_user.uuid == self.uuid or
-        current_user.can? write: self.owner_uuid_was
-      # current user is, or has :write permission on, the previous owner
-      return true
-    else
-      logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
-      return false
-    end
+    return true
   end
 
   def ensure_permission_to_destroy
index b88d4a50d037a30a80e8fcec6d9269dd384ef1a2..21d249b625792b6ce0d38aeb49b076390426a749 100644 (file)
@@ -119,7 +119,7 @@ class Node < ArvadosModel
   end
 
   def start!(ping_url_method)
-    ensure_permission_to_update
+    ensure_permission_to_save
     ping_url = ping_url_method.call({ id: self.uuid, ping_secret: self.info[:ping_secret] })
     if (Rails.configuration.compute_node_ec2run_args and
         Rails.configuration.compute_node_ami)
index 8e7d7fca27e4f172ef92bf76ae16156e49827d52..f851c588c7445ef7d180bee1896b9f2e0bfc7d36 100644 (file)
@@ -44,7 +44,9 @@ module CurrentApiClient
   def system_user
     if not $system_user
       real_current_user = Thread.current[:user]
-      Thread.current[:user] = User.new(is_admin: true, is_active: true)
+      Thread.current[:user] = User.new(is_admin: true,
+                                       is_active: true,
+                                       uuid: system_user_uuid)
       $system_user = User.where('uuid=?', system_user_uuid).first
       if !$system_user
         $system_user = User.new(uuid: system_user_uuid,
index beb061cff79a1245cf292c7a25841e39964eac95..79bddf01cee1e64d483cf21e57fda4ae391e442e 100644 (file)
@@ -2,12 +2,14 @@
 
 trusted_workbench:
   uuid: zzzzz-ozdt8-teyxzyd8qllg11h
+  owner_uuid: zzzzz-tpzed-000000000000000
   name: Official Workbench
   url_prefix: https://official-workbench.local/
   is_trusted: true
 
 untrusted:
   uuid: zzzzz-ozdt8-obw7foaks3qjyej
+  owner_uuid: zzzzz-tpzed-000000000000000
   name: Untrusted
   url_prefix: https://untrusted.local/
   is_trusted: false
index 947d762dac0109dd1f2b2d0280fd7a21e54f7fc8..96db93c35e1e1c812910654b78f98665ef833499 100644 (file)
@@ -67,3 +67,23 @@ asubfolder:
   name: A Subfolder
   description: "Test folder belonging to active user's first test folder"
   group_class: folder
+
+bad_group_has_ownership_cycle_a:
+  uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+  owner_uuid: zzzzz-j7d0g-0077nzts8c178lw
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-05-03 18:50:08 -0400
+  modified_at: 2014-05-03 18:50:08 -0400
+  updated_at: 2014-05-03 18:50:08 -0400
+  name: Owned by bad group b
+
+bad_group_has_ownership_cycle_b:
+  uuid: zzzzz-j7d0g-0077nzts8c178lw
+  owner_uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-05-03 18:50:08 -0400
+  modified_at: 2014-05-03 18:50:08 -0400
+  updated_at: 2014-05-03 18:50:08 -0400
+  name: Owned by bad group a
index 87997df93e0165fcd460cd529608763c09218c26..304e2d1f4e660ee101fc4fd0eae2c02eccba612a 100644 (file)
@@ -345,3 +345,17 @@ foo_collection_tag:
   link_class: tag
   name: foo_tag
   properties: {}
+
+active_user_can_manage_bad_group_cx2al9cqkmsf1hs:
+  uuid: zzzzz-o0j2j-ezv55ahzc9lvjwe
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-05-03 18:50:08 -0400
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-000000000000000
+  modified_at: 2014-05-03 18:50:08 -0400
+  updated_at: 2014-05-03 18:50:08 -0400
+  tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  link_class: permission
+  name: can_manage
+  head_uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+  properties: {}
index 869d87f7db19f24d63743b04d410404ba4379082..0e8c56b6d26537a053e366b63f764fbacecdba48 100644 (file)
@@ -33,6 +33,13 @@ class ActiveSupport::TestCase
     Thread.current[:user] = nil
   end
 
+  def set_user_from_auth(auth_name)
+    client_auth = api_client_authorizations(auth_name)
+    Thread.current[:api_client_authorization] = client_auth
+    Thread.current[:api_client] = client_auth.api_client
+    Thread.current[:user] = client_auth.user
+  end
+
   def expect_json
     self.request.headers["Accept"] = "text/json"
   end
index 778eb0c58112da09ed327f989964963b1fbde577..6b419ad64a621870aca2b04d1a232a485aa35327 100644 (file)
@@ -1,7 +1,61 @@
 require 'test_helper'
 
 class GroupTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
+
+  test "cannot set owner_uuid to object with existing ownership cycle" do
+    set_user_from_auth :active_trustedclient
+
+    # First make sure we have lots of permission on the bad group
+    g = groups(:bad_group_has_ownership_cycle_b)
+    g.name += " xyz"
+    assert g.save, "active user should be able to modify group #{g.uuid}"
+
+    # Use the group as the owner of a new object
+    s = Specimen.
+      create(owner_uuid: groups(:bad_group_has_ownership_cycle_b).uuid)
+    assert s.valid?, "ownership should pass validation"
+    assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
+
+    # Use the group as the new owner of an existing object
+    s = specimens(:in_afolder)
+    s.owner_uuid = groups(:bad_group_has_ownership_cycle_b).uuid
+    assert s.valid?, "ownership should pass validation"
+    assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
+  end
+
+  test "cannot create a new ownership cycle" do
+    set_user_from_auth :active_trustedclient
+
+    g_foo = Group.create(name: "foo")
+    g_foo.save!
+
+    g_bar = Group.create(name: "bar")
+    g_bar.save!
+
+    g_foo.owner_uuid = g_bar.uuid
+    assert g_foo.save, lambda { g_foo.errors.messages }
+    g_bar.owner_uuid = g_foo.uuid
+    assert g_bar.valid?, "ownership cycle should not prevent validation"
+    assert_equal false, g_bar.save, "should not create an ownership loop"
+    assert g_bar.errors.messages[:owner_uuid].join(" ").match(/ownership cycle/)
+  end
+
+  test "cannot create a single-object ownership cycle" do
+    set_user_from_auth :active_trustedclient
+
+    g_foo = Group.create(name: "foo")
+    assert g_foo.save
+
+    # Ensure I have permission to manage this group even when its owner changes
+    perm_link = Link.create(tail_uuid: users(:active).uuid,
+                            head_uuid: g_foo.uuid,
+                            link_class: 'permission',
+                            name: 'can_manage')
+    assert perm_link.save
+
+    g_foo.owner_uuid = g_foo.uuid
+    assert_equal false, g_foo.save, "should not create an ownership loop"
+    assert g_foo.errors.messages[:owner_uuid].join(" ").match(/ownership cycle/)
+  end
+
 end
index be8498d4bac3883621df2688dcf4ad5423f8c900..4fc273be73d20335c971496d6eb50eae65c90225 100644 (file)
@@ -65,13 +65,6 @@ class LogTest < ActiveSupport::TestCase
     end
   end
 
-  def set_user_from_auth(auth_name)
-    client_auth = api_client_authorizations(auth_name)
-    Thread.current[:api_client_authorization] = client_auth
-    Thread.current[:api_client] = client_auth.api_client
-    Thread.current[:user] = client_auth.user
-  end
-
   test "creating a user makes a log" do
     set_user_from_auth :admin_trustedclient
     u = User.new(first_name: "Log", last_name: "Test")