Merge branch '21666-provision-test-improvement'
[arvados.git] / services / api / app / models / link.rb
index e4ba7f3de1ef8f20833355efb0dae1a153b05113..2eb6b88a0c864a5ea33f8dbe7a5a4c9fe4cd2a1f 100644 (file)
@@ -14,10 +14,15 @@ class Link < ArvadosModel
   validate :name_links_are_obsolete
   validate :permission_to_attach_to_objects
   before_update :restrict_alter_permissions
-  after_update :call_update_permissions
-  after_create :call_update_permissions
+  before_update :apply_max_overlapping_permissions
+  before_create :apply_max_overlapping_permissions
+  after_update :delete_overlapping_permissions
+  after_update :call_update_permissions, :if => Proc.new { @need_update_permissions }
+  after_create :call_update_permissions, :if => Proc.new { @need_update_permissions }
   before_destroy :clear_permissions
+  after_destroy :delete_overlapping_permissions
   after_destroy :check_permissions
+  before_save :check_need_update_permissions
 
   api_accessible :user, extend: :common do |t|
     t.add :tail_uuid
@@ -29,6 +34,58 @@ class Link < ArvadosModel
     t.add :properties
   end
 
+  PermLevel = {
+    'can_read' => 0,
+    'can_write' => 1,
+    'can_manage' => 2,
+  }
+
+  def apply_max_overlapping_permissions
+    return if self.link_class != 'permission' || !PermLevel[self.name]
+    Link.
+      lock. # select ... for update
+      where(link_class: 'permission',
+            tail_uuid: self.tail_uuid,
+            head_uuid: self.head_uuid,
+            name: PermLevel.keys).
+      where('uuid <> ?', self.uuid).each do |other|
+      if PermLevel[other.name] > PermLevel[self.name]
+        self.name = other.name
+      end
+    end
+  end
+
+  def delete_overlapping_permissions
+    return if self.link_class != 'permission'
+    redundant = nil
+    if PermLevel[self.name]
+      redundant = Link.
+                    lock. # select ... for update
+                    where(link_class: 'permission',
+                          tail_uuid: self.tail_uuid,
+                          head_uuid: self.head_uuid,
+                          name: PermLevel.keys).
+                    where('uuid <> ?', self.uuid)
+    elsif self.name == 'can_login' &&
+          self.properties.respond_to?(:has_key?) &&
+          self.properties.has_key?('username')
+      redundant = Link.
+                    lock. # select ... for update
+                    where(link_class: 'permission',
+                          tail_uuid: self.tail_uuid,
+                          head_uuid: self.head_uuid,
+                          name: 'can_login').
+                    where('properties @> ?', SafeJSON.dump({'username' => self.properties['username']})).
+                    where('uuid <> ?', self.uuid)
+    end
+    if redundant
+      redundant.each do |link|
+        link.clear_permissions
+      end
+      redundant.delete_all
+    end
+  end
+
   def head_kind
     if k = ArvadosModel::resource_class_for_uuid(head_uuid)
       k.kind
@@ -43,6 +100,28 @@ class Link < ArvadosModel
 
   protected
 
+  def check_readable_uuid attr, attr_value
+    if attr == 'tail_uuid' &&
+       !attr_value.nil? &&
+       self.link_class == 'permission' &&
+       attr_value[0..4] != Rails.configuration.ClusterID &&
+       ApiClientAuthorization.remote_host(uuid_prefix: attr_value[0..4]) &&
+       ArvadosModel::resource_class_for_uuid(attr_value) == User
+      # Permission link tail is a remote user (the user permissions
+      # are being granted to), so bypass the standard check that a
+      # referenced object uuid is readable by current user.
+      #
+      # We could do a call to the remote cluster to check if the user
+      # in tail_uuid exists.  This would detect copy-and-paste errors,
+      # but add another way for the request to fail, and I don't think
+      # it would improve security.  It doesn't seem to be worth the
+      # complexity tradeoff.
+      true
+    else
+      super
+    end
+  end
+
   def permission_to_attach_to_objects
     # Anonymous users cannot write links
     return false if !current_user
@@ -76,6 +155,11 @@ class Link < ArvadosModel
 
     head_obj = ArvadosModel.find_by_uuid(head_uuid)
 
+    if head_obj.nil?
+      errors.add(:head_uuid, "does not exist")
+      return false
+    end
+
     # No permission links can be pointed to past collection versions
     if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
       errors.add(:head_uuid, "cannot point to a past version of a collection")
@@ -106,15 +190,19 @@ class Link < ArvadosModel
     'can_manage' => 3,
   }
 
+  def check_need_update_permissions
+    @need_update_permissions = self.link_class == 'permission' && (name != name_was || new_record?)
+  end
+
   def call_update_permissions
-    if self.link_class == 'permission'
       update_permissions tail_uuid, head_uuid, PERM_LEVEL[name], self.uuid
-    end
+      current_user.forget_cached_group_perms
   end
 
   def clear_permissions
     if self.link_class == 'permission'
       update_permissions tail_uuid, head_uuid, REVOKE_PERM, self.uuid
+      current_user.forget_cached_group_perms
     end
   end