1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
5 # Protect referential integrity of owner_uuid columns in other tables
6 # that can refer to the uuid column in this table.
10 def self.included(base)
11 base.extend(ClassMethods)
13 # Rails' "has_many" can prevent us from destroying the owner
14 # record when other objects refer to it.
15 ActiveRecord::Base.connection.tables.each do |t|
16 next if t == base.table_name
18 # in-use tables that should be skipped
19 'ar_internal_metadata',
20 'permission_refresh_lock',
23 # obsolete tables from removed APIs
31 'materialized_permissions',
39 klass = t.classify.constantize
40 next unless klass and 'owner_uuid'.in?(klass.columns.collect(&:name))
41 base.has_many(t.to_sym,
42 foreign_key: 'owner_uuid',
44 dependent: :restrict_with_exception)
46 # We need custom protection for changing an owner's primary
47 # key. (Apart from this restriction, admins are allowed to change
49 base.validate :restrict_uuid_change_breaking_associations
53 def install_view(type)
54 conn = ActiveRecord::Base.connection
56 # Check whether the temporary view has already been created
57 # during this connection. If not, create it.
58 conn.exec_query "SAVEPOINT check_#{type}_view"
60 conn.exec_query("SELECT 1 FROM #{type}_view LIMIT 0")
62 conn.exec_query "ROLLBACK TO SAVEPOINT check_#{type}_view"
63 sql = File.read(Rails.root.join("lib", "create_#{type}_view.sql"))
66 conn.exec_query "RELEASE SAVEPOINT check_#{type}_view"
72 def descendant_project_uuids
73 self.class.install_view('ancestor')
74 ActiveRecord::Base.connection.
75 exec_query('SELECT ancestor_view.uuid
77 LEFT JOIN groups ON groups.uuid=ancestor_view.uuid
78 WHERE ancestor_uuid = $1 AND groups.group_class = $2',
79 # "name" arg is a query label that appears in logs:
80 "descendant_project_uuids for #{self.uuid}",
81 # "binds" arg is an array of [col_id, value] for '$1' vars:
82 [self.uuid, 'project'],
83 ).rows.map do |project_uuid,|
90 def restrict_uuid_change_breaking_associations
91 return true if new_record? or not uuid_changed?
93 # Check for objects that have my old uuid listed as their owner.
94 self.class.reflect_on_all_associations(:has_many).each do |assoc|
95 next unless assoc.foreign_key == 'owner_uuid'
96 if assoc.klass.where(owner_uuid: uuid_was).any?
98 "cannot be changed on a #{self.class} that owns objects")
103 # if I owned myself before, I'll just continue to own myself with
105 if owner_uuid == uuid_was
106 self.owner_uuid = uuid