Merge branch '19929-fill-discovery-document'
[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.in?([
18                       # in-use tables that should be skipped
19                       'ar_internal_metadata',
20                       'permission_refresh_lock',
21                       'schema_migrations',
22                       'uuid_locks',
23                       # obsolete tables from removed APIs
24                       'api_clients',
25                       'commit_ancestors',
26                       'commits',
27                       'humans',
28                       'jobs',
29                       'job_tasks',
30                       'keep_disks',
31                       'materialized_permissions',
32                       'nodes',
33                       'pipeline_instances',
34                       'pipeline_templates',
35                       'repositories',
36                       'specimens',
37                       'traits',
38                     ])
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',
43                     primary_key: 'uuid',
44                     dependent: :restrict_with_exception)
45     end
46     # We need custom protection for changing an owner's primary
47     # key. (Apart from this restriction, admins are allowed to change
48     # UUIDs.)
49     base.validate :restrict_uuid_change_breaking_associations
50   end
51
52   module ClassMethods
53     def install_view(type)
54       conn = ActiveRecord::Base.connection
55       transaction do
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"
59         begin
60           conn.exec_query("SELECT 1 FROM #{type}_view LIMIT 0")
61         rescue
62           conn.exec_query "ROLLBACK TO SAVEPOINT check_#{type}_view"
63           sql = File.read(Rails.root.join("lib", "create_#{type}_view.sql"))
64           conn.exec_query(sql)
65         ensure
66           conn.exec_query "RELEASE SAVEPOINT check_#{type}_view"
67         end
68       end
69     end
70   end
71
72   def descendant_project_uuids
73     self.class.install_view('ancestor')
74     ActiveRecord::Base.connection.
75       exec_query('SELECT ancestor_view.uuid
76                   FROM ancestor_view
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,|
84       project_uuid
85     end
86   end
87
88   protected
89
90   def restrict_uuid_change_breaking_associations
91     return true if new_record? or not uuid_changed?
92
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?
97         errors.add(:uuid,
98                    "cannot be changed on a #{self.class} that owns objects")
99         return false
100       end
101     end
102
103     # if I owned myself before, I'll just continue to own myself with
104     # my new uuid.
105     if owner_uuid == uuid_was
106       self.owner_uuid = uuid
107     end
108   end
109 end