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