Merge branch '3036-collection-uuids' closes #3036
[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   before_validation :normalize_collection_uuids
25   validate :ensure_serialized_attribute_type
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.select do |col|
56       case col.type
57       when :string, :text
58         true
59       when :datetime, :integer, :boolean
60         !textonly_operator
61       else
62         false
63       end
64     end.map(&:name)
65   end
66
67   def self.attribute_column attr
68     self.columns.select { |col| col.name == attr.to_s }.first
69   end
70
71   def self.attributes_required_columns
72     # This method returns a hash.  Each key is the name of an API attribute,
73     # and it's mapped to a list of database columns that must be fetched
74     # to generate that attribute.
75     # This implementation generates a simple map of attributes to
76     # matching column names.  Subclasses can override this method
77     # to specify that method-backed API attributes need to fetch
78     # specific columns from the database.
79     all_columns = columns.map(&:name)
80     api_column_map = Hash.new { |hash, key| hash[key] = [] }
81     methods.grep(/^api_accessible_\w+$/).each do |method_name|
82       next if method_name == :api_accessible_attributes
83       send(method_name).each_pair do |api_attr_name, col_name|
84         col_name = col_name.to_s
85         if all_columns.include?(col_name)
86           api_column_map[api_attr_name.to_s] |= [col_name]
87         end
88       end
89     end
90     api_column_map
91   end
92
93   # If current user can manage the object, return an array of uuids of
94   # users and groups that have permission to write the object. The
95   # first two elements are always [self.owner_uuid, current user's
96   # uuid].
97   #
98   # If current user can write but not manage the object, return
99   # [self.owner_uuid, current user's uuid].
100   #
101   # If current user cannot write this object, just return
102   # [self.owner_uuid].
103   def writable_by
104     unless (owner_uuid == current_user.uuid or
105             current_user.is_admin or
106             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
107       if current_user.groups_i_can(:write).index(uuid)
108         return [owner_uuid, current_user.uuid]
109       else
110         return [owner_uuid]
111       end
112     end
113     [owner_uuid, current_user.uuid] + permissions.collect do |p|
114       if ['can_write', 'can_manage'].index p.name
115         p.tail_uuid
116       end
117     end.compact.uniq
118   end
119
120   # Return a query with read permissions restricted to the union of of the
121   # permissions of the members of users_list, i.e. if something is readable by
122   # any user in users_list, it will be readable in the query returned by this
123   # function.
124   def self.readable_by(*users_list)
125     # Get rid of troublesome nils
126     users_list.compact!
127
128     # Load optional keyword arguments, if they exist.
129     if users_list.last.is_a? Hash
130       kwargs = users_list.pop
131     else
132       kwargs = {}
133     end
134
135     # Check if any of the users are admin.  If so, we're done.
136     if users_list.select { |u| u.is_admin }.any?
137       return self
138     end
139
140     # Collect the uuids for each user and any groups readable by each user.
141     user_uuids = users_list.map { |u| u.uuid }
142     uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
143     sql_conds = []
144     sql_params = []
145     sql_table = kwargs.fetch(:table_name, table_name)
146     or_object_uuid = ''
147
148     # This row is owned by a member of users_list, or owned by a group
149     # readable by a member of users_list
150     # or
151     # This row uuid is the uuid of a member of users_list
152     # or
153     # A permission link exists ('write' and 'manage' implicitly include
154     # 'read') from a member of users_list, or a group readable by users_list,
155     # to this row, or to the owner of this row (see join() below).
156     sql_conds += ["#{sql_table}.uuid in (?)"]
157     sql_params += [user_uuids]
158
159     if uuid_list.any?
160       sql_conds += ["#{sql_table}.owner_uuid in (?)"]
161       sql_params += [uuid_list]
162
163       sanitized_uuid_list = uuid_list.
164         collect { |uuid| sanitize(uuid) }.join(', ')
165       permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
166       sql_conds += ["#{sql_table}.uuid IN #{permitted_uuids}"]
167     end
168
169     if sql_table == "links" and users_list.any?
170       # This row is a 'permission' or 'resources' link class
171       # The uuid for a member of users_list is referenced in either the head
172       # or tail of the link
173       sql_conds += ["(#{sql_table}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND (#{sql_table}.head_uuid IN (?) OR #{sql_table}.tail_uuid IN (?)))"]
174       sql_params += [user_uuids, user_uuids]
175     end
176
177     if sql_table == "logs" and users_list.any?
178       # Link head points to the object described by this row
179       sql_conds += ["#{sql_table}.object_uuid IN #{permitted_uuids}"]
180
181       # This object described by this row is owned by this user, or owned by a group readable by this user
182       sql_conds += ["#{sql_table}.object_owner_uuid in (?)"]
183       sql_params += [uuid_list]
184     end
185
186     # Link head points to this row, or to the owner of this row (the
187     # thing to be read)
188     #
189     # Link tail originates from this user, or a group that is readable
190     # by this user (the identity with authorization to read)
191     #
192     # Link class is 'permission' ('write' and 'manage' implicitly
193     # include 'read')
194     where(sql_conds.join(' OR '), *sql_params)
195   end
196
197   def logged_attributes
198     attributes
199   end
200
201   def has_permission? perm_type, target_uuid
202     Link.where(link_class: "permission",
203                name: perm_type,
204                tail_uuid: uuid,
205                head_uuid: target_uuid).any?
206   end
207
208   protected
209
210   def ensure_ownership_path_leads_to_user
211     if new_record? or owner_uuid_changed?
212       uuid_in_path = {owner_uuid => true, uuid => true}
213       x = owner_uuid
214       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
215         begin
216           if x == uuid
217             # Test for cycles with the new version, not the DB contents
218             x = owner_uuid
219           elsif !owner_class.respond_to? :find_by_uuid
220             raise ActiveRecord::RecordNotFound.new
221           else
222             x = owner_class.find_by_uuid(x).owner_uuid
223           end
224         rescue ActiveRecord::RecordNotFound => e
225           errors.add :owner_uuid, "is not owned by any user: #{e}"
226           return false
227         end
228         if uuid_in_path[x]
229           if x == owner_uuid
230             errors.add :owner_uuid, "would create an ownership cycle"
231           else
232             errors.add :owner_uuid, "has an ownership cycle"
233           end
234           return false
235         end
236         uuid_in_path[x] = true
237       end
238     end
239     true
240   end
241
242   def ensure_owner_uuid_is_permitted
243     raise PermissionDeniedError if !current_user
244
245     if new_record? and respond_to? :owner_uuid=
246       self.owner_uuid ||= current_user.uuid
247     end
248
249     if self.owner_uuid.nil?
250       errors.add :owner_uuid, "cannot be nil"
251       raise PermissionDeniedError
252     end
253
254     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
255     unless rsc_class == User or rsc_class == Group
256       errors.add :owner_uuid, "must be set to User or Group"
257       raise PermissionDeniedError
258     end
259
260     # Verify "write" permission on old owner
261     # default fail unless one of:
262     # owner_uuid did not change
263     # previous owner_uuid is nil
264     # current user is the old owner
265     # current user is this object
266     # current user can_write old owner
267     unless !owner_uuid_changed? or
268         owner_uuid_was.nil? or
269         current_user.uuid == self.owner_uuid_was or
270         current_user.uuid == self.uuid or
271         current_user.can? write: self.owner_uuid_was
272       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write old owner_uuid #{owner_uuid_was}"
273       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
274       raise PermissionDeniedError
275     end
276
277     # Verify "write" permission on new owner
278     # default fail unless one of:
279     # current_user is this object
280     # current user can_write new owner
281     unless current_user == self or current_user.can? write: owner_uuid
282       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write new owner_uuid #{owner_uuid}"
283       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
284       raise PermissionDeniedError
285     end
286
287     true
288   end
289
290   def ensure_permission_to_save
291     unless (new_record? ? permission_to_create : permission_to_update)
292       raise PermissionDeniedError
293     end
294   end
295
296   def permission_to_create
297     current_user.andand.is_active
298   end
299
300   def permission_to_update
301     if !current_user
302       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
303       return false
304     end
305     if !current_user.is_active
306       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
307       return false
308     end
309     return true if current_user.is_admin
310     if self.uuid_changed?
311       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
312       return false
313     end
314     return true
315   end
316
317   def ensure_permission_to_destroy
318     raise PermissionDeniedError unless permission_to_destroy
319   end
320
321   def permission_to_destroy
322     permission_to_update
323   end
324
325   def maybe_update_modified_by_fields
326     update_modified_by_fields if self.changed? or self.new_record?
327     true
328   end
329
330   def update_modified_by_fields
331     self.updated_at = Time.now
332     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
333     self.modified_at = Time.now
334     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
335     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
336     true
337   end
338
339   def self.has_symbols? x
340     if x.is_a? Hash
341       x.each do |k,v|
342         return true if has_symbols?(k) or has_symbols?(v)
343       end
344       false
345     elsif x.is_a? Array
346       x.each do |k|
347         return true if has_symbols?(k)
348       end
349       false
350     else
351       (x.class == Symbol)
352     end
353   end
354
355   def self.recursive_stringify x
356     if x.is_a? Hash
357       Hash[x.collect do |k,v|
358              [recursive_stringify(k), recursive_stringify(v)]
359            end]
360     elsif x.is_a? Array
361       x.collect do |k|
362         recursive_stringify k
363       end
364     elsif x.is_a? Symbol
365       x.to_s
366     else
367       x
368     end
369   end
370
371   def ensure_serialized_attribute_type
372     # Specifying a type in the "serialize" declaration causes rails to
373     # raise an exception if a different data type is retrieved from
374     # the database during load().  The validation preventing such
375     # crash-inducing records from being inserted in the database in
376     # the first place seems to have been left as an exercise to the
377     # developer.
378     self.class.serialized_attributes.each do |colname, attr|
379       if attr.object_class
380         if self.attributes[colname].class != attr.object_class
381           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
382         elsif self.class.has_symbols? attributes[colname]
383           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
384         end
385       end
386     end
387   end
388
389   def convert_serialized_symbols_to_strings
390     # ensure_serialized_attribute_type should prevent symbols from
391     # getting into the database in the first place. If someone managed
392     # to get them into the database (perhaps using an older version)
393     # we'll convert symbols to strings when loading from the
394     # database. (Otherwise, loading and saving an object with existing
395     # symbols in a serialized field will crash.)
396     self.class.serialized_attributes.each do |colname, attr|
397       if self.class.has_symbols? attributes[colname]
398         attributes[colname] = self.class.recursive_stringify attributes[colname]
399         self.send(colname + '=',
400                   self.class.recursive_stringify(attributes[colname]))
401       end
402     end
403   end
404
405   def foreign_key_attributes
406     attributes.keys.select { |a| a.match /_uuid$/ }
407   end
408
409   def skip_uuid_read_permission_check
410     %w(modified_by_client_uuid)
411   end
412
413   def skip_uuid_existence_check
414     []
415   end
416
417   def normalize_collection_uuids
418     foreign_key_attributes.each do |attr|
419       attr_value = send attr
420       if attr_value.is_a? String and
421           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
422         begin
423           send "#{attr}=", Collection.normalize_uuid(attr_value)
424         rescue
425           # TODO: abort instead of silently accepting unnormalizable value?
426         end
427       end
428     end
429   end
430
431   @@prefixes_hash = nil
432   def self.uuid_prefixes
433     unless @@prefixes_hash
434       @@prefixes_hash = {}
435       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
436         if k.respond_to?(:uuid_prefix)
437           @@prefixes_hash[k.uuid_prefix] = k
438         end
439       end
440     end
441     @@prefixes_hash
442   end
443
444   def self.uuid_like_pattern
445     "_____-#{uuid_prefix}-_______________"
446   end
447
448   def ensure_valid_uuids
449     specials = [system_user_uuid]
450
451     foreign_key_attributes.each do |attr|
452       if new_record? or send (attr + "_changed?")
453         next if skip_uuid_existence_check.include? attr
454         attr_value = send attr
455         next if specials.include? attr_value
456         if attr_value
457           if (r = ArvadosModel::resource_class_for_uuid attr_value)
458             unless skip_uuid_read_permission_check.include? attr
459               r = r.readable_by(current_user)
460             end
461             if r.where(uuid: attr_value).count == 0
462               errors.add(attr, "'#{attr_value}' not found")
463             end
464           end
465         end
466       end
467     end
468   end
469
470   class Email
471     def self.kind
472       "email"
473     end
474
475     def kind
476       self.class.kind
477     end
478
479     def self.readable_by (*u)
480       self
481     end
482
483     def self.where (u)
484       [{:uuid => u[:uuid]}]
485     end
486   end
487
488   def self.resource_class_for_uuid(uuid)
489     if uuid.is_a? ArvadosModel
490       return uuid.class
491     end
492     unless uuid.is_a? String
493       return nil
494     end
495     resource_class = nil
496
497     Rails.application.eager_load!
498     uuid.match HasUuid::UUID_REGEX do |re|
499       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
500     end
501
502     if uuid.match /.+@.+/
503       return Email
504     end
505
506     nil
507   end
508
509   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
510   # an object in any class.
511   def self.find_by_uuid uuid
512     if self == ArvadosModel
513       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
514       # delegate to the appropriate subclass based on the given uuid.
515       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
516     else
517       super
518     end
519   end
520
521   def log_start_state
522     @old_etag = etag
523     @old_attributes = logged_attributes
524   end
525
526   def log_change(event_type)
527     log = Log.new(event_type: event_type).fill_object(self)
528     yield log
529     log.save!
530     connection.execute "NOTIFY logs, '#{log.id}'"
531     log_start_state
532   end
533
534   def log_create
535     log_change('create') do |log|
536       log.fill_properties('old', nil, nil)
537       log.update_to self
538     end
539   end
540
541   def log_update
542     log_change('update') do |log|
543       log.fill_properties('old', @old_etag, @old_attributes)
544       log.update_to self
545     end
546   end
547
548   def log_destroy
549     log_change('destroy') do |log|
550       log.fill_properties('old', @old_etag, @old_attributes)
551       log.update_to nil
552     end
553   end
554 end