X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/1441208182ced30e28f05611fbfa51674570dd1e..HEAD:/services/api/app/models/group.rb diff --git a/services/api/app/models/group.rb b/services/api/app/models/group.rb index 8565b2a417..d4c81fe9d1 100644 --- a/services/api/app/models/group.rb +++ b/services/api/app/models/group.rb @@ -4,6 +4,7 @@ require 'can_be_an_owner' require 'trashable' +require 'update_priorities' class Group < ArvadosModel include HasUuid @@ -19,9 +20,11 @@ class Group < ArvadosModel validate :ensure_filesystem_compatible_name validate :check_group_class validate :check_filter_group_filters + validate :check_frozen_state_change_allowed 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 @@ -29,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 @@ -40,6 +44,25 @@ class Group < ArvadosModel t.add :trash_at 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 @@ -92,37 +115,118 @@ class Group < ArvadosModel end end + def check_frozen_state_change_allowed + if frozen_by_uuid == "" + self.frozen_by_uuid = nil + end + if frozen_by_uuid_changed? || (new_record? && frozen_by_uuid) + if group_class != "project" + errors.add(:frozen_by_uuid, "cannot be modified on a non-project group") + return + end + if frozen_by_uuid_was && Rails.configuration.API.UnfreezeProjectRequiresAdmin && !current_user.is_admin + errors.add(:frozen_by_uuid, "can only be changed by an admin user, once set") + return + end + if frozen_by_uuid && frozen_by_uuid != current_user.uuid + errors.add(:frozen_by_uuid, "can only be set to the current user's UUID") + return + end + 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") + end + Rails.configuration.API.FreezeProjectRequiresProperties.andand.each do |key, _| + key = key.to_s + if !properties[key] || properties[key] == "" + errors.add(:frozen_by_uuid, "can only be set if properties[#{key}] value is non-empty") + end + end + end + end + 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_delete %{ -delete from trashed_groups where group_uuid in (select target_uuid from #{temptable} where trash_at is NULL); + 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) + +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? - 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; + if frozen_by_uuid + rows = ActiveRecord::Base.connection.exec_query(%{ +with temptable as (select * from project_subtree_with_is_frozen($1,$2)) + +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 @@ -138,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 @@ -164,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 @@ -181,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