Merge branch 'master' into 14670-new-java-sdk-docs
[arvados.git] / services / api / lib / can_be_an_owner.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 # Protect referential integrity of owner_uuid columns in other tables
6 # that can refer to the uuid column in this table.
7
8 module CanBeAnOwner
9
10   def self.included(base)
11     base.extend(ClassMethods)
12
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
17       next if t == 'schema_migrations'
18       next if t == 'permission_refresh_lock'
19       next if t == 'ar_internal_metadata'
20       klass = t.classify.constantize
21       next unless klass and 'owner_uuid'.in?(klass.columns.collect(&:name))
22       base.has_many(t.to_sym,
23                     foreign_key: :owner_uuid,
24                     primary_key: :uuid,
25                     dependent: :restrict_with_exception)
26     end
27     # We need custom protection for changing an owner's primary
28     # key. (Apart from this restriction, admins are allowed to change
29     # UUIDs.)
30     base.validate :restrict_uuid_change_breaking_associations
31   end
32
33   module ClassMethods
34     def install_view(type)
35       conn = ActiveRecord::Base.connection
36       transaction do
37         # Check whether the temporary view has already been created
38         # during this connection. If not, create it.
39         conn.exec_query "SAVEPOINT check_#{type}_view"
40         begin
41           conn.exec_query("SELECT 1 FROM #{type}_view LIMIT 0")
42         rescue
43           conn.exec_query "ROLLBACK TO SAVEPOINT check_#{type}_view"
44           sql = File.read(Rails.root.join("lib", "create_#{type}_view.sql"))
45           conn.exec_query(sql)
46         ensure
47           conn.exec_query "RELEASE SAVEPOINT check_#{type}_view"
48         end
49       end
50     end
51   end
52
53   def descendant_project_uuids
54     self.class.install_view('ancestor')
55     ActiveRecord::Base.connection.
56       exec_query('SELECT ancestor_view.uuid
57                   FROM ancestor_view
58                   LEFT JOIN groups ON groups.uuid=ancestor_view.uuid
59                   WHERE ancestor_uuid = $1 AND groups.group_class = $2',
60                   # "name" arg is a query label that appears in logs:
61                   "descendant_project_uuids for #{self.uuid}",
62                   # "binds" arg is an array of [col_id, value] for '$1' vars:
63                   [[nil, self.uuid], [nil, 'project']],
64                   ).rows.map do |project_uuid,|
65       project_uuid
66     end
67   end
68
69   protected
70
71   def restrict_uuid_change_breaking_associations
72     return true if new_record? or not uuid_changed?
73
74     # Check for objects that have my old uuid listed as their owner.
75     self.class.reflect_on_all_associations(:has_many).each do |assoc|
76       next unless assoc.foreign_key == :owner_uuid
77       if assoc.klass.where(owner_uuid: uuid_was).any?
78         errors.add(:uuid,
79                    "cannot be changed on a #{self.class} that owns objects")
80         return false
81       end
82     end
83
84     # if I owned myself before, I'll just continue to own myself with
85     # my new uuid.
86     if owner_uuid == uuid_was
87       self.owner_uuid = uuid
88     end
89   end
90 end