Convert joins to subqueries to fix duplicates in owned_items
[arvados.git] / services / api / app / models / arvados_model.rb
1 require 'assign_uuid'
2 class ArvadosModel < ActiveRecord::Base
3   self.abstract_class = true
4
5   include CurrentApiClient      # current_user, current_api_client, etc.
6
7   attr_protected :created_at
8   attr_protected :modified_by_user_uuid
9   attr_protected :modified_by_client_uuid
10   attr_protected :modified_at
11   after_initialize :log_start_state
12   before_create :ensure_permission_to_create
13   before_update :ensure_permission_to_update
14   before_destroy :ensure_permission_to_destroy
15
16   before_create :update_modified_by_fields
17   before_update :maybe_update_modified_by_fields
18   after_create :log_create
19   after_update :log_update
20   after_destroy :log_destroy
21   validate :ensure_serialized_attribute_type
22   validate :normalize_collection_uuids
23   validate :ensure_valid_uuids
24
25   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
26
27   class PermissionDeniedError < StandardError
28     def http_status
29       403
30     end
31   end
32
33   class UnauthorizedError < StandardError
34     def http_status
35       401
36     end
37   end
38
39   def self.kind_class(kind)
40     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
41   end
42
43   def href
44     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
45   end
46
47   def self.searchable_columns operator
48     textonly_operator = !operator.match(/[<=>]/)
49     self.columns.collect do |col|
50       if col.name == 'owner_uuid'
51         nil
52       elsif [:string, :text].index(col.type)
53         col.name
54       elsif !textonly_operator and [:datetime, :integer].index(col.type)
55         col.name
56       end
57     end.compact
58   end
59
60   def self.attribute_column attr
61     self.columns.select { |col| col.name == attr.to_s }.first
62   end
63
64   # def eager_load_associations
65   #   self.class.columns.each do |col|
66   #     re = col.name.match /^(.*)_kind$/
67   #     if (re and
68   #         self.respond_to? re[1].to_sym and
69   #         (auuid = self.send((re[1] + '_uuid').to_sym)) and
70   #         (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
71   #         (aobject = aclass.where('uuid=?', auuid).first))
72   #       self.instance_variable_set('@'+re[1], aobject)
73   #     end
74   #   end
75   # end
76
77   def self.readable_by user
78     uuid_list = [user.uuid, *user.groups_i_can(:read)]
79     sanitized_uuid_list = uuid_list.
80       collect { |uuid| sanitize(uuid) }.join(', ')
81     or_references_me = ''
82     if self == Link and user
83       or_references_me = "OR (#{table_name}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND #{sanitize user.uuid} IN (#{table_name}.head_uuid, #{table_name}.tail_uuid))"
84     end
85     where("?=? OR #{table_name}.owner_uuid in (?) OR #{table_name}.uuid=? OR #{table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list})) #{or_references_me}",
86           true, user.is_admin,
87           uuid_list,
88           user.uuid)
89   end
90
91   def logged_attributes
92     attributes
93   end
94
95   protected
96
97   def ensure_permission_to_create
98     raise PermissionDeniedError unless permission_to_create
99   end
100
101   def permission_to_create
102     current_user.andand.is_active
103   end
104
105   def ensure_permission_to_update
106     raise PermissionDeniedError unless permission_to_update
107   end
108
109   def permission_to_update
110     if !current_user
111       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
112       return false
113     end
114     if !current_user.is_active
115       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
116       return false
117     end
118     return true if current_user.is_admin
119     if self.uuid_changed?
120       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
121       return false
122     end
123     if self.owner_uuid_changed?
124       if current_user.uuid == self.owner_uuid or
125           current_user.can? write: self.owner_uuid
126         # current_user is, or has :write permission on, the new owner
127       else
128         logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
129         return false
130       end
131     end
132     if current_user.uuid == self.owner_uuid_was or
133         current_user.uuid == self.uuid or
134         current_user.can? write: self.owner_uuid_was
135       # current user is, or has :write permission on, the previous owner
136       return true
137     else
138       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
139       return false
140     end
141   end
142
143   def ensure_permission_to_destroy
144     raise PermissionDeniedError unless permission_to_destroy
145   end
146
147   def permission_to_destroy
148     permission_to_update
149   end
150
151   def maybe_update_modified_by_fields
152     update_modified_by_fields if self.changed? or self.new_record?
153   end
154
155   def update_modified_by_fields
156     self.updated_at = Time.now
157     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
158     self.modified_at = Time.now
159     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
160     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
161   end
162
163   def ensure_serialized_attribute_type
164     # Specifying a type in the "serialize" declaration causes rails to
165     # raise an exception if a different data type is retrieved from
166     # the database during load().  The validation preventing such
167     # crash-inducing records from being inserted in the database in
168     # the first place seems to have been left as an exercise to the
169     # developer.
170     self.class.serialized_attributes.each do |colname, attr|
171       if attr.object_class
172         unless self.attributes[colname].is_a? attr.object_class
173           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}"
174         end
175       end
176     end
177   end
178
179   def foreign_key_attributes
180     attributes.keys.select { |a| a.match /_uuid$/ }
181   end
182
183   def skip_uuid_read_permission_check
184     %w(modified_by_client_uuid)
185   end
186
187   def skip_uuid_existence_check
188     []
189   end
190
191   def normalize_collection_uuids
192     foreign_key_attributes.each do |attr|
193       attr_value = send attr
194       if attr_value.is_a? String and
195           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
196         begin
197           send "#{attr}=", Collection.normalize_uuid(attr_value)
198         rescue
199           # TODO: abort instead of silently accepting unnormalizable value?
200         end
201       end
202     end
203   end
204
205   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
206
207   @@prefixes_hash = nil
208   def self.uuid_prefixes
209     unless @@prefixes_hash
210       @@prefixes_hash = {}
211       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
212         if k.respond_to?(:uuid_prefix)
213           @@prefixes_hash[k.uuid_prefix] = k
214         end
215       end
216     end
217     @@prefixes_hash
218   end
219
220   def self.uuid_like_pattern
221     "_____-#{uuid_prefix}-_______________"
222   end
223
224   def ensure_valid_uuids
225     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
226
227     foreign_key_attributes.each do |attr|
228       if new_record? or send (attr + "_changed?")
229         next if skip_uuid_existence_check.include? attr
230         attr_value = send attr
231         next if specials.include? attr_value
232         if attr_value
233           if (r = ArvadosModel::resource_class_for_uuid attr_value)
234             unless skip_uuid_read_permission_check.include? attr
235               r = r.readable_by(current_user)
236             end
237             if r.where(uuid: attr_value).count == 0
238               errors.add(attr, "'#{attr_value}' not found")
239             end
240           end
241         end
242       end
243     end
244   end
245
246   class Email
247     def self.kind
248       "email"
249     end
250
251     def kind
252       self.class.kind
253     end
254
255     def self.readable_by (u)
256       self
257     end
258
259     def self.where (u)
260       [{:uuid => u[:uuid]}]
261     end
262   end
263
264   def self.resource_class_for_uuid(uuid)
265     if uuid.is_a? ArvadosModel
266       return uuid.class
267     end
268     unless uuid.is_a? String
269       return nil
270     end
271     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
272       return Collection
273     end
274     resource_class = nil
275
276     Rails.application.eager_load!
277     uuid.match @@UUID_REGEX do |re|
278       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
279     end
280
281     if uuid.match /.+@.+/
282       return Email
283     end
284
285     nil
286   end
287
288   def log_start_state
289     @old_etag = etag
290     @old_attributes = logged_attributes
291   end
292
293   def log_change(event_type)
294     log = Log.new(event_type: event_type).fill_object(self)
295     yield log
296     log.save!
297     log_start_state
298   end
299
300   def log_create
301     log_change('create') do |log|
302       log.fill_properties('old', nil, nil)
303       log.update_to self
304     end
305   end
306
307   def log_update
308     log_change('update') do |log|
309       log.fill_properties('old', @old_etag, @old_attributes)
310       log.update_to self
311     end
312   end
313
314   def log_destroy
315     log_change('destroy') do |log|
316       log.fill_properties('old', @old_etag, @old_attributes)
317       log.update_to nil
318     end
319   end
320 end