Merge branch '21666-provision-test-improvement'
[arvados.git] / services / api / app / models / group.rb
index d6f698432c86cd97ce5170ff563b687927f5d7a4..d4c81fe9d1d9cf2c558d644bf45bf2f495dc9ef6 100644 (file)
@@ -4,6 +4,7 @@
 
 require 'can_be_an_owner'
 require 'trashable'
+require 'update_priorities'
 
 class Group < ArvadosModel
   include HasUuid
@@ -23,6 +24,7 @@ class Group < ArvadosModel
   before_create :assign_name
   after_create :after_ownership_change
   after_create :update_trash
+  after_create :update_frozen
 
   before_update :before_ownership_change
   after_update :after_ownership_change
@@ -30,7 +32,8 @@ class Group < ArvadosModel
   after_create :add_role_manage_link
 
   after_update :update_trash
-  before_destroy :clear_permissions_and_trash
+  after_update :update_frozen
+  before_destroy :clear_permissions_trash_frozen
 
   api_accessible :user, extend: :common do |t|
     t.add :name
@@ -42,6 +45,24 @@ class Group < ArvadosModel
     t.add :is_trashed
     t.add :properties
     t.add :frozen_by_uuid
+    t.add :can_write
+    t.add :can_manage
+  end
+
+  # check if admins are allowed to make changes to the project, e.g. it
+  # isn't trashed or frozen.
+  def admin_change_permitted
+    !(FrozenGroup.where(uuid: self.uuid).any? || TrashedGroup.where(group_uuid: self.uuid).any?)
+  end
+
+  protected
+
+  def self.attributes_required_columns
+    super.merge(
+                'can_write' => ['owner_uuid', 'uuid'],
+                'can_manage' => ['owner_uuid', 'uuid'],
+                'writable_by' => ['owner_uuid', 'uuid'],
+                )
   end
 
   def ensure_filesystem_compatible_name
@@ -114,6 +135,9 @@ class Group < ArvadosModel
       if !new_record? && !current_user.can?(manage: uuid)
         raise PermissionDeniedError
       end
+      if trash_at || delete_at || (!new_record? && TrashedGroup.where(group_uuid: uuid).any?)
+        errors.add(:frozen_by_uuid, "cannot be set on a trashed project")
+      end
       if frozen_by_uuid_was.nil?
         if Rails.configuration.API.FreezeProjectRequiresDescription && !attribute_present?(:description)
           errors.add(:frozen_by_uuid, "can only be set if description is non-empty")
@@ -129,36 +153,80 @@ class Group < ArvadosModel
   end
 
   def update_trash
-    if saved_change_to_trash_at? or saved_change_to_owner_uuid?
-      # The group was added or removed from the trash.
-      #
-      # Strategy:
-      #   Compute project subtree, propagating trash_at to subprojects
-      #   Remove groups that don't belong from trash
-      #   Add/update groups that do belong in the trash
-
-      temptable = "group_subtree_#{rand(2**64).to_s(10)}"
-      ActiveRecord::Base.connection.exec_query %{
-create temporary table #{temptable} on commit drop
-as select * from project_subtree_with_trash_at($1, LEAST($2, $3)::timestamp)
+    return unless saved_change_to_trash_at? || saved_change_to_owner_uuid?
+
+    # The group was added or removed from the trash.
+    #
+    # Strategy:
+    #   Compute project subtree, propagating trash_at to subprojects
+    #   Ensure none of the newly trashed descendants were frozen (if so, bail out)
+    #   Remove groups that don't belong from trash
+    #   Add/update groups that do belong in the trash
+
+    frozen_descendants = ActiveRecord::Base.connection.exec_query(%{
+with temptable as (select * from project_subtree_with_trash_at($1, LEAST($2, $3)::timestamp))
+      select uuid from frozen_groups, temptable where uuid = target_uuid
 },
-                                               'Group.update_trash.select',
-                                               [[nil, self.uuid],
-                                                [nil, TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at],
-                                                [nil, self.trash_at]]
+      "Group.update_trash.select",
+      [self.uuid,
+       TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at,
+       self.trash_at])
+    if frozen_descendants.any?
+      raise ArgumentError.new("cannot trash project containing frozen project #{frozen_descendants[0]["uuid"]}")
+    end
+
+    ActiveRecord::Base.connection.exec_query(%{
+with temptable as (select * from project_subtree_with_trash_at($1, LEAST($2, $3)::timestamp)),
+
+delete_rows as (delete from trashed_groups where group_uuid in (select target_uuid from temptable where trash_at is NULL)),
+
+insert_rows as (insert into trashed_groups (group_uuid, trash_at)
+  select target_uuid as group_uuid, trash_at from temptable where trash_at is not NULL
+  on conflict (group_uuid) do update set trash_at=EXCLUDED.trash_at)
 
-      ActiveRecord::Base.connection.exec_delete %{
-delete from trashed_groups where group_uuid in (select target_uuid from #{temptable} where trash_at is NULL);
+select container_uuid from container_requests where
+  owner_uuid in (select target_uuid from temptable) and
+  requesting_container_uuid is NULL and state = 'Committed' and container_uuid is not NULL
 },
-                                            "Group.update_trash.delete"
+      "Group.update_trash.select",
+      [self.uuid,
+       TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at,
+       self.trash_at]).each do |container_uuid|
+      update_priorities container_uuid["container_uuid"]
+    end
+  end
+
+  def update_frozen
+    return unless saved_change_to_frozen_by_uuid? || saved_change_to_owner_uuid?
+
+    if frozen_by_uuid
+      rows = ActiveRecord::Base.connection.exec_query(%{
+with temptable as (select * from project_subtree_with_is_frozen($1,$2))
 
-      ActiveRecord::Base.connection.exec_query %{
-insert into trashed_groups (group_uuid, trash_at)
-  select target_uuid as group_uuid, trash_at from #{temptable} where trash_at is not NULL
-on conflict (group_uuid) do update set trash_at=EXCLUDED.trash_at;
+select cr.uuid, cr.state from container_requests cr, temptable frozen
+  where cr.owner_uuid = frozen.uuid and frozen.is_frozen
+  and cr.state not in ($3, $4) limit 1
 },
-                                            "Group.update_trash.insert"
+                                                      "Group.update_frozen.check_container_requests",
+                                                      [self.uuid,
+                                                       !self.frozen_by_uuid.nil?,
+                                                       ContainerRequest::Uncommitted,
+                                                       ContainerRequest::Final])
+      if rows.any?
+        raise ArgumentError.new("cannot freeze project containing container request #{rows.first['uuid']} with state = #{rows.first['state']}")
+      end
     end
+
+ActiveRecord::Base.connection.exec_query(%{
+with temptable as (select * from project_subtree_with_is_frozen($1,$2)),
+
+delete_rows as (delete from frozen_groups where uuid in (select uuid from temptable where not is_frozen))
+
+insert into frozen_groups (uuid) select uuid from temptable where is_frozen on conflict do nothing
+}, "Group.update_frozen.update",
+                                         [self.uuid,
+                                          !self.frozen_by_uuid.nil?])
+
   end
 
   def before_ownership_change
@@ -174,12 +242,16 @@ on conflict (group_uuid) do update set trash_at=EXCLUDED.trash_at;
     end
   end
 
-  def clear_permissions_and_trash
+  def clear_permissions_trash_frozen
     MaterializedPermission.where(target_uuid: uuid).delete_all
-    ActiveRecord::Base.connection.exec_delete %{
-delete from trashed_groups where group_uuid=$1
-}, "Group.clear_permissions_and_trash", [[nil, self.uuid]]
-
+    ActiveRecord::Base.connection.exec_delete(
+      "delete from trashed_groups where group_uuid=$1",
+      "Group.clear_permissions_trash_frozen",
+      [self.uuid])
+    ActiveRecord::Base.connection.exec_delete(
+      "delete from frozen_groups where uuid=$1",
+      "Group.clear_permissions_trash_frozen",
+      [self.uuid])
   end
 
   def assign_name
@@ -200,7 +272,7 @@ delete from trashed_groups where group_uuid=$1
       if self.owner_uuid != system_user_uuid
         raise "Owner uuid for role must be system user"
       end
-      raise PermissionDeniedError unless current_user.can?(manage: uuid)
+      raise PermissionDeniedError.new("role group cannot be modified without can_manage permission") unless current_user.can?(manage: uuid)
       true
     else
       super
@@ -217,4 +289,31 @@ delete from trashed_groups where group_uuid=$1
       end
     end
   end
+
+  def permission_to_create
+    if !super
+      return false
+    elsif group_class == "role" &&
+       !Rails.configuration.Users.CanCreateRoleGroups &&
+       !current_user.andand.is_admin
+      raise PermissionDeniedError.new("this cluster does not allow users to create role groups")
+    else
+      return true
+    end
+  end
+
+  def permission_to_update
+    if !super
+      return false
+    elsif frozen_by_uuid && frozen_by_uuid_was
+      errors.add :uuid, "#{uuid} is frozen and cannot be modified"
+      return false
+    else
+      return true
+    end
+  end
+
+  def self.full_text_searchable_columns
+    super - ["frozen_by_uuid"]
+  end
 end