2883: Improved pagination control a bit, added search.
[arvados.git] / services / api / app / models / arvados_model.rb
1 require 'has_uuid'
2
3 class ArvadosModel < ActiveRecord::Base
4   self.abstract_class = true
5
6   include CurrentApiClient      # current_user, current_api_client, etc.
7
8   attr_protected :created_at
9   attr_protected :modified_by_user_uuid
10   attr_protected :modified_by_client_uuid
11   attr_protected :modified_at
12   after_initialize :log_start_state
13   before_save :ensure_permission_to_save
14   before_save :ensure_owner_uuid_is_permitted
15   before_save :ensure_ownership_path_leads_to_user
16   before_destroy :ensure_owner_uuid_is_permitted
17   before_destroy :ensure_permission_to_destroy
18   before_create :update_modified_by_fields
19   before_update :maybe_update_modified_by_fields
20   after_create :log_create
21   after_update :log_update
22   after_destroy :log_destroy
23   after_find :convert_serialized_symbols_to_strings
24   validate :ensure_serialized_attribute_type
25   validate :normalize_collection_uuids
26   validate :ensure_valid_uuids
27
28   # Note: This only returns permission links. It does not account for
29   # permissions obtained via user.is_admin or
30   # user.uuid==object.owner_uuid.
31   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
32
33   class PermissionDeniedError < StandardError
34     def http_status
35       403
36     end
37   end
38
39   class UnauthorizedError < StandardError
40     def http_status
41       401
42     end
43   end
44
45   def self.kind_class(kind)
46     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
47   end
48
49   def href
50     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
51   end
52
53   def self.searchable_columns operator
54     textonly_operator = !operator.match(/[<=>]/)
55     self.columns.collect do |col|
56       if [:string, :text].index(col.type)
57         col.name
58       elsif !textonly_operator and [:datetime, :integer].index(col.type)
59         col.name
60       end
61     end.compact
62   end
63
64   def self.attribute_column attr
65     self.columns.select { |col| col.name == attr.to_s }.first
66   end
67
68   # Return nil if current user is not allowed to see the list of
69   # writers. Otherwise, return a list of user_ and group_uuids with
70   # write permission. (If not returning nil, current_user is always in
71   # the list because can_manage permission is needed to see the list
72   # of writers.)
73   def writable_by
74     unless (owner_uuid == current_user.uuid or
75             current_user.is_admin or
76             current_user.groups_i_can(:manage).index(owner_uuid))
77       return nil
78     end
79     [owner_uuid, current_user.uuid] + permissions.collect do |p|
80       if ['can_write', 'can_manage'].index p.name
81         p.tail_uuid
82       end
83     end.compact.uniq
84   end
85
86   # Return a query with read permissions restricted to the union of of the
87   # permissions of the members of users_list, i.e. if something is readable by
88   # any user in users_list, it will be readable in the query returned by this
89   # function.
90   def self.readable_by(*users_list)
91     # Get rid of troublesome nils
92     users_list.compact!
93
94     # Check if any of the users are admin.  If so, we're done.
95     if users_list.select { |u| u.is_admin }.empty?
96
97       # Collect the uuids for each user and any groups readable by each user.
98       user_uuids = users_list.map { |u| u.uuid }
99       uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
100       sanitized_uuid_list = uuid_list.
101         collect { |uuid| sanitize(uuid) }.join(', ')
102       sql_conds = []
103       sql_params = []
104       or_object_uuid = ''
105
106       # This row is owned by a member of users_list, or owned by a group
107       # readable by a member of users_list
108       # or
109       # This row uuid is the uuid of a member of users_list
110       # or
111       # A permission link exists ('write' and 'manage' implicitly include
112       # 'read') from a member of users_list, or a group readable by users_list,
113       # to this row, or to the owner of this row (see join() below).
114       permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
115
116       sql_conds += ["#{table_name}.owner_uuid in (?)",
117                     "#{table_name}.uuid in (?)",
118                     "#{table_name}.uuid IN #{permitted_uuids}"]
119       sql_params += [uuid_list, user_uuids]
120
121       if self == Link and users_list.any?
122         # This row is a 'permission' or 'resources' link class
123         # The uuid for a member of users_list is referenced in either the head
124         # or tail of the link
125         sql_conds += ["(#{table_name}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND (#{table_name}.head_uuid IN (?) OR #{table_name}.tail_uuid IN (?)))"]
126         sql_params += [user_uuids, user_uuids]
127       end
128
129       if self == Log and users_list.any?
130         # Link head points to the object described by this row
131         sql_conds += ["#{table_name}.object_uuid IN #{permitted_uuids}"]
132
133         # This object described by this row is owned by this user, or owned by a group readable by this user
134         sql_conds += ["#{table_name}.object_owner_uuid in (?)"]
135         sql_params += [uuid_list]
136       end
137
138       # Link head points to this row, or to the owner of this row (the thing to be read)
139       #
140       # Link tail originates from this user, or a group that is readable by this
141       # user (the identity with authorization to read)
142       #
143       # Link class is 'permission' ('write' and 'manage' implicitly include 'read')
144       where(sql_conds.join(' OR '), *sql_params)
145     else
146       # At least one user is admin, so don't bother to apply any restrictions.
147       self
148     end
149   end
150
151   def logged_attributes
152     attributes
153   end
154
155   protected
156
157   def ensure_ownership_path_leads_to_user
158     if new_record? or owner_uuid_changed?
159       uuid_in_path = {owner_uuid => true, uuid => true}
160       x = owner_uuid
161       while (owner_class = self.class.resource_class_for_uuid(x)) != User
162         begin
163           if x == uuid
164             # Test for cycles with the new version, not the DB contents
165             x = owner_uuid
166           elsif !owner_class.respond_to? :find_by_uuid
167             raise ActiveRecord::RecordNotFound.new
168           else
169             x = owner_class.find_by_uuid(x).owner_uuid
170           end
171         rescue ActiveRecord::RecordNotFound => e
172           errors.add :owner_uuid, "is not owned by any user: #{e}"
173           return false
174         end
175         if uuid_in_path[x]
176           if x == owner_uuid
177             errors.add :owner_uuid, "would create an ownership cycle"
178           else
179             errors.add :owner_uuid, "has an ownership cycle"
180           end
181           return false
182         end
183         uuid_in_path[x] = true
184       end
185     end
186     true
187   end
188
189   def ensure_owner_uuid_is_permitted
190     raise PermissionDeniedError if !current_user
191     if respond_to? :owner_uuid=
192       self.owner_uuid ||= current_user.uuid
193     end
194     if self.owner_uuid_changed?
195       if new_record?
196         return true
197       elsif current_user.uuid == self.owner_uuid or
198           current_user.can? write: self.owner_uuid
199         # current_user is, or has :write permission on, the new owner
200       else
201         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}"
202         raise PermissionDeniedError
203       end
204     end
205     if new_record?
206       return true
207     elsif current_user.uuid == self.owner_uuid_was or
208         current_user.uuid == self.uuid or
209         current_user.can? write: self.owner_uuid_was
210       # current user is, or has :write permission on, the previous owner
211       return true
212     else
213       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}"
214       raise PermissionDeniedError
215     end
216   end
217
218   def ensure_permission_to_save
219     unless (new_record? ? permission_to_create : permission_to_update)
220       raise PermissionDeniedError
221     end
222   end
223
224   def permission_to_create
225     current_user.andand.is_active
226   end
227
228   def permission_to_update
229     if !current_user
230       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
231       return false
232     end
233     if !current_user.is_active
234       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
235       return false
236     end
237     return true if current_user.is_admin
238     if self.uuid_changed?
239       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
240       return false
241     end
242     return true
243   end
244
245   def ensure_permission_to_destroy
246     raise PermissionDeniedError unless permission_to_destroy
247   end
248
249   def permission_to_destroy
250     permission_to_update
251   end
252
253   def maybe_update_modified_by_fields
254     update_modified_by_fields if self.changed? or self.new_record?
255     true
256   end
257
258   def update_modified_by_fields
259     self.updated_at = Time.now
260     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
261     self.modified_at = Time.now
262     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
263     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
264     true
265   end
266
267   def self.has_symbols? x
268     if x.is_a? Hash
269       x.each do |k,v|
270         return true if has_symbols?(k) or has_symbols?(v)
271       end
272       false
273     elsif x.is_a? Array
274       x.each do |k|
275         return true if has_symbols?(k)
276       end
277       false
278     else
279       (x.class == Symbol)
280     end
281   end
282
283   def self.recursive_stringify x
284     if x.is_a? Hash
285       Hash[x.collect do |k,v|
286              [recursive_stringify(k), recursive_stringify(v)]
287            end]
288     elsif x.is_a? Array
289       x.collect do |k|
290         recursive_stringify k
291       end
292     elsif x.is_a? Symbol
293       x.to_s
294     else
295       x
296     end
297   end
298
299   def ensure_serialized_attribute_type
300     # Specifying a type in the "serialize" declaration causes rails to
301     # raise an exception if a different data type is retrieved from
302     # the database during load().  The validation preventing such
303     # crash-inducing records from being inserted in the database in
304     # the first place seems to have been left as an exercise to the
305     # developer.
306     self.class.serialized_attributes.each do |colname, attr|
307       if attr.object_class
308         if self.attributes[colname].class != attr.object_class
309           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
310         elsif self.class.has_symbols? attributes[colname]
311           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
312         end
313       end
314     end
315   end
316
317   def convert_serialized_symbols_to_strings
318     # ensure_serialized_attribute_type should prevent symbols from
319     # getting into the database in the first place. If someone managed
320     # to get them into the database (perhaps using an older version)
321     # we'll convert symbols to strings when loading from the
322     # database. (Otherwise, loading and saving an object with existing
323     # symbols in a serialized field will crash.)
324     self.class.serialized_attributes.each do |colname, attr|
325       if self.class.has_symbols? attributes[colname]
326         attributes[colname] = self.class.recursive_stringify attributes[colname]
327         self.send(colname + '=',
328                   self.class.recursive_stringify(attributes[colname]))
329       end
330     end
331   end
332
333   def foreign_key_attributes
334     attributes.keys.select { |a| a.match /_uuid$/ }
335   end
336
337   def skip_uuid_read_permission_check
338     %w(modified_by_client_uuid)
339   end
340
341   def skip_uuid_existence_check
342     []
343   end
344
345   def normalize_collection_uuids
346     foreign_key_attributes.each do |attr|
347       attr_value = send attr
348       if attr_value.is_a? String and
349           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
350         begin
351           send "#{attr}=", Collection.normalize_uuid(attr_value)
352         rescue
353           # TODO: abort instead of silently accepting unnormalizable value?
354         end
355       end
356     end
357   end
358
359   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
360
361   @@prefixes_hash = nil
362   def self.uuid_prefixes
363     unless @@prefixes_hash
364       @@prefixes_hash = {}
365       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
366         if k.respond_to?(:uuid_prefix)
367           @@prefixes_hash[k.uuid_prefix] = k
368         end
369       end
370     end
371     @@prefixes_hash
372   end
373
374   def self.uuid_like_pattern
375     "_____-#{uuid_prefix}-_______________"
376   end
377
378   def ensure_valid_uuids
379     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
380
381     foreign_key_attributes.each do |attr|
382       if new_record? or send (attr + "_changed?")
383         next if skip_uuid_existence_check.include? attr
384         attr_value = send attr
385         next if specials.include? attr_value
386         if attr_value
387           if (r = ArvadosModel::resource_class_for_uuid attr_value)
388             unless skip_uuid_read_permission_check.include? attr
389               r = r.readable_by(current_user)
390             end
391             if r.where(uuid: attr_value).count == 0
392               errors.add(attr, "'#{attr_value}' not found")
393             end
394           end
395         end
396       end
397     end
398   end
399
400   class Email
401     def self.kind
402       "email"
403     end
404
405     def kind
406       self.class.kind
407     end
408
409     def self.readable_by (*u)
410       self
411     end
412
413     def self.where (u)
414       [{:uuid => u[:uuid]}]
415     end
416   end
417
418   def self.resource_class_for_uuid(uuid)
419     if uuid.is_a? ArvadosModel
420       return uuid.class
421     end
422     unless uuid.is_a? String
423       return nil
424     end
425     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
426       return Collection
427     end
428     resource_class = nil
429
430     Rails.application.eager_load!
431     uuid.match @@UUID_REGEX do |re|
432       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
433     end
434
435     if uuid.match /.+@.+/
436       return Email
437     end
438
439     nil
440   end
441
442   def log_start_state
443     @old_etag = etag
444     @old_attributes = logged_attributes
445   end
446
447   def log_change(event_type)
448     log = Log.new(event_type: event_type).fill_object(self)
449     yield log
450     log.save!
451     connection.execute "NOTIFY logs, '#{log.id}'"
452     log_start_state
453   end
454
455   def log_create
456     log_change('create') do |log|
457       log.fill_properties('old', nil, nil)
458       log.update_to self
459     end
460   end
461
462   def log_update
463     log_change('update') do |log|
464       log.fill_properties('old', @old_etag, @old_attributes)
465       log.update_to self
466     end
467   end
468
469   def log_destroy
470     log_change('destroy') do |log|
471       log.fill_properties('old', @old_etag, @old_attributes)
472       log.update_to nil
473     end
474   end
475 end