14873: Adds new abstract class ApplicationRecord.
[arvados.git] / services / api / app / models / arvados_model.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'arvados_model_updates'
6 require 'has_uuid'
7 require 'record_filters'
8 require 'serializers'
9 require 'request_error'
10
11 class ArvadosModel < ApplicationRecord
12   self.abstract_class = true
13
14   include ArvadosModelUpdates
15   include CurrentApiClient      # current_user, current_api_client, etc.
16   include DbCurrentTime
17   extend RecordFilters
18
19   after_initialize :log_start_state
20   before_save :ensure_permission_to_save
21   before_save :ensure_owner_uuid_is_permitted
22   before_save :ensure_ownership_path_leads_to_user
23   before_destroy :ensure_owner_uuid_is_permitted
24   before_destroy :ensure_permission_to_destroy
25   before_create :update_modified_by_fields
26   before_update :maybe_update_modified_by_fields
27   after_create :log_create
28   after_update :log_update
29   after_destroy :log_destroy
30   after_find :convert_serialized_symbols_to_strings
31   before_validation :normalize_collection_uuids
32   before_validation :set_default_owner
33   validate :ensure_valid_uuids
34
35   # Note: This only returns permission links. It does not account for
36   # permissions obtained via user.is_admin or
37   # user.uuid==object.owner_uuid.
38   has_many(:permissions,
39            ->{where(link_class: 'permission')},
40            foreign_key: :head_uuid,
41            class_name: 'Link',
42            primary_key: :uuid)
43
44   # If async is true at create or update, permission graph
45   # update is deferred allowing making multiple calls without the performance
46   # penalty.
47   attr_accessor :async_permissions_update
48
49   # Ignore listed attributes on mass assignments
50   def self.protected_attributes
51     []
52   end
53
54   class PermissionDeniedError < RequestError
55     def http_status
56       403
57     end
58   end
59
60   class AlreadyLockedError < RequestError
61     def http_status
62       422
63     end
64   end
65
66   class LockFailedError < RequestError
67     def http_status
68       422
69     end
70   end
71
72   class InvalidStateTransitionError < RequestError
73     def http_status
74       422
75     end
76   end
77
78   class UnauthorizedError < RequestError
79     def http_status
80       401
81     end
82   end
83
84   class UnresolvableContainerError < RequestError
85     def http_status
86       422
87     end
88   end
89
90   def self.kind_class(kind)
91     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
92   end
93
94   def href
95     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
96   end
97
98   def self.permit_attribute_params raw_params
99     # strong_parameters does not provide security: permissions are
100     # implemented with before_save hooks.
101     #
102     # The following permit! is necessary even with
103     # "ActionController::Parameters.permit_all_parameters = true",
104     # because permit_all does not permit nested attributes.
105     raw_params ||= {}
106
107     if raw_params
108       raw_params = raw_params.to_hash
109       raw_params.delete_if { |k, _| self.protected_attributes.include? k }
110       serialized_attributes.each do |colname, coder|
111         param = raw_params[colname.to_sym]
112         if param.nil?
113           # ok
114         elsif !param.is_a?(coder.object_class)
115           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
116         elsif has_nonstring_keys?(param)
117           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
118         end
119       end
120     end
121     ActionController::Parameters.new(raw_params).permit!
122   end
123
124   def initialize raw_params={}, *args
125     super(self.class.permit_attribute_params(raw_params), *args)
126   end
127
128   # Reload "old attributes" for logging, too.
129   def reload(*args)
130     super
131     log_start_state
132   end
133
134   def self.create raw_params={}, *args
135     super(permit_attribute_params(raw_params), *args)
136   end
137
138   def update_attributes raw_params={}, *args
139     super(self.class.permit_attribute_params(raw_params), *args)
140   end
141
142   def self.selectable_attributes(template=:user)
143     # Return an array of attribute name strings that can be selected
144     # in the given template.
145     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
146   end
147
148   def self.searchable_columns operator
149     textonly_operator = !operator.match(/[<=>]/)
150     self.columns.select do |col|
151       case col.type
152       when :string, :text
153         true
154       when :datetime, :integer, :boolean
155         !textonly_operator
156       else
157         false
158       end
159     end.map(&:name)
160   end
161
162   def self.attribute_column attr
163     self.columns.select { |col| col.name == attr.to_s }.first
164   end
165
166   def self.attributes_required_columns
167     # This method returns a hash.  Each key is the name of an API attribute,
168     # and it's mapped to a list of database columns that must be fetched
169     # to generate that attribute.
170     # This implementation generates a simple map of attributes to
171     # matching column names.  Subclasses can override this method
172     # to specify that method-backed API attributes need to fetch
173     # specific columns from the database.
174     all_columns = columns.map(&:name)
175     api_column_map = Hash.new { |hash, key| hash[key] = [] }
176     methods.grep(/^api_accessible_\w+$/).each do |method_name|
177       next if method_name == :api_accessible_attributes
178       send(method_name).each_pair do |api_attr_name, col_name|
179         col_name = col_name.to_s
180         if all_columns.include?(col_name)
181           api_column_map[api_attr_name.to_s] |= [col_name]
182         end
183       end
184     end
185     api_column_map
186   end
187
188   def self.ignored_select_attributes
189     ["href", "kind", "etag"]
190   end
191
192   def self.columns_for_attributes(select_attributes)
193     if select_attributes.empty?
194       raise ArgumentError.new("Attribute selection list cannot be empty")
195     end
196     api_column_map = attributes_required_columns
197     invalid_attrs = []
198     select_attributes.each do |s|
199       next if ignored_select_attributes.include? s
200       if not s.is_a? String or not api_column_map.include? s
201         invalid_attrs << s
202       end
203     end
204     if not invalid_attrs.empty?
205       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
206     end
207     # Given an array of attribute names to select, return an array of column
208     # names that must be fetched from the database to satisfy the request.
209     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
210   end
211
212   def self.default_orders
213     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
214   end
215
216   def self.unique_columns
217     ["id", "uuid"]
218   end
219
220   def self.limit_index_columns_read
221     # This method returns a list of column names.
222     # If an index request reads that column from the database,
223     # APIs that return lists will only fetch objects until reaching
224     # max_index_database_read bytes of data from those columns.
225     []
226   end
227
228   # If current user can manage the object, return an array of uuids of
229   # users and groups that have permission to write the object. The
230   # first two elements are always [self.owner_uuid, current user's
231   # uuid].
232   #
233   # If current user can write but not manage the object, return
234   # [self.owner_uuid, current user's uuid].
235   #
236   # If current user cannot write this object, just return
237   # [self.owner_uuid].
238   def writable_by
239     return [owner_uuid] if not current_user
240     unless (owner_uuid == current_user.uuid or
241             current_user.is_admin or
242             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
243       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
244           [uuid, owner_uuid]).any?
245         return [owner_uuid, current_user.uuid]
246       else
247         return [owner_uuid]
248       end
249     end
250     [owner_uuid, current_user.uuid] + permissions.collect do |p|
251       if ['can_write', 'can_manage'].index p.name
252         p.tail_uuid
253       end
254     end.compact.uniq
255   end
256
257   # Return a query with read permissions restricted to the union of the
258   # permissions of the members of users_list, i.e. if something is readable by
259   # any user in users_list, it will be readable in the query returned by this
260   # function.
261   def self.readable_by(*users_list)
262     # Get rid of troublesome nils
263     users_list.compact!
264
265     # Load optional keyword arguments, if they exist.
266     if users_list.last.is_a? Hash
267       kwargs = users_list.pop
268     else
269       kwargs = {}
270     end
271
272     # Collect the UUIDs of the authorized users.
273     sql_table = kwargs.fetch(:table_name, table_name)
274     include_trash = kwargs.fetch(:include_trash, false)
275     include_old_versions = kwargs.fetch(:include_old_versions, false)
276
277     sql_conds = nil
278     user_uuids = users_list.map { |u| u.uuid }
279
280     exclude_trashed_records = ""
281     if !include_trash and (sql_table == "groups" or sql_table == "collections") then
282       # Only include records that are not explicitly trashed
283       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
284     end
285
286     if users_list.select { |u| u.is_admin }.any?
287       # Admin skips most permission checks, but still want to filter on trashed items.
288       if !include_trash
289         if sql_table != "api_client_authorizations"
290           # Only include records where the owner is not trashed
291           sql_conds = "#{sql_table}.owner_uuid NOT IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
292                       "WHERE trashed = 1) #{exclude_trashed_records}"
293         end
294       end
295     else
296       trashed_check = ""
297       if !include_trash then
298         trashed_check = "AND trashed = 0"
299       end
300
301       # Note: it is possible to combine the direct_check and
302       # owner_check into a single EXISTS() clause, however it turns
303       # out query optimizer doesn't like it and forces a sequential
304       # table scan.  Constructing the query with separate EXISTS()
305       # clauses enables it to use the index.
306       #
307       # see issue 13208 for details.
308
309       # Match a direct read permission link from the user to the record uuid
310       direct_check = "#{sql_table}.uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
311                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check})"
312
313       # Match a read permission link from the user to the record's owner_uuid
314       owner_check = ""
315       if sql_table != "api_client_authorizations" and sql_table != "groups" then
316         owner_check = "OR #{sql_table}.owner_uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
317           "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_owner_uuid IS NOT NULL) "
318       end
319
320       links_cond = ""
321       if sql_table == "links"
322         # Match any permission link that gives one of the authorized
323         # users some permission _or_ gives anyone else permission to
324         # view one of the authorized users.
325         links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
326                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
327       end
328
329       sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
330
331     end
332
333     if !include_old_versions && sql_table == "collections"
334       exclude_old_versions = "#{sql_table}.uuid = #{sql_table}.current_version_uuid"
335       if sql_conds.nil?
336         sql_conds = exclude_old_versions
337       else
338         sql_conds += " AND #{exclude_old_versions}"
339       end
340     end
341
342     self.where(sql_conds,
343                user_uuids: user_uuids,
344                permission_link_classes: ['permission', 'resources'])
345   end
346
347   def save_with_unique_name!
348     uuid_was = uuid
349     name_was = name
350     max_retries = 2
351     transaction do
352       conn = ActiveRecord::Base.connection
353       conn.exec_query 'SAVEPOINT save_with_unique_name'
354       begin
355         save!
356       rescue ActiveRecord::RecordNotUnique => rn
357         raise if max_retries == 0
358         max_retries -= 1
359
360         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
361
362         # Dig into the error to determine if it is specifically calling out a
363         # (owner_uuid, name) uniqueness violation.  In this specific case, and
364         # the client requested a unique name with ensure_unique_name==true,
365         # update the name field and try to save again.  Loop as necessary to
366         # discover a unique name.  It is necessary to handle name choosing at
367         # this level (as opposed to the client) to ensure that record creation
368         # never fails due to a race condition.
369         err = rn.cause
370         raise unless err.is_a?(PG::UniqueViolation)
371
372         # Unfortunately ActiveRecord doesn't abstract out any of the
373         # necessary information to figure out if this the error is actually
374         # the specific case where we want to apply the ensure_unique_name
375         # behavior, so the following code is specialized to Postgres.
376         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
377         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
378
379         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
380         if new_name == name
381           # If the database is fast enough to do two attempts in the
382           # same millisecond, we need to wait to ensure we try a
383           # different timestamp on each attempt.
384           sleep 0.002
385           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
386         end
387
388         self[:name] = new_name
389         if uuid_was.nil? && !uuid.nil?
390           self[:uuid] = nil
391           if self.is_a? Collection
392             # Reset so that is assigned to the new UUID
393             self[:current_version_uuid] = nil
394           end
395         end
396         conn.exec_query 'SAVEPOINT save_with_unique_name'
397         retry
398       ensure
399         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
400       end
401     end
402   end
403
404   def logged_attributes
405     attributes.except(*Rails.configuration.unlogged_attributes)
406   end
407
408   def self.full_text_searchable_columns
409     self.columns.select do |col|
410       [:string, :text, :jsonb].include?(col.type)
411     end.map(&:name)
412   end
413
414   def self.full_text_tsvector
415     parts = full_text_searchable_columns.collect do |column|
416       cast = serialized_attributes[column] ? '::text' : ''
417       "coalesce(#{column}#{cast},'')"
418     end
419     "to_tsvector('english', substr(#{parts.join(" || ' ' || ")}, 0, 8000))"
420   end
421
422   def self.apply_filters query, filters
423     ft = record_filters filters, self
424     if not ft[:cond_out].any?
425       return query
426     end
427     query.where('(' + ft[:cond_out].join(') AND (') + ')',
428                           *ft[:param_out])
429   end
430
431   protected
432
433   def self.deep_sort_hash(x)
434     if x.is_a? Hash
435       x.sort.collect do |k, v|
436         [k, deep_sort_hash(v)]
437       end.to_h
438     elsif x.is_a? Array
439       x.collect { |v| deep_sort_hash(v) }
440     else
441       x
442     end
443   end
444
445   def ensure_ownership_path_leads_to_user
446     if new_record? or owner_uuid_changed?
447       uuid_in_path = {owner_uuid => true, uuid => true}
448       x = owner_uuid
449       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
450         begin
451           if x == uuid
452             # Test for cycles with the new version, not the DB contents
453             x = owner_uuid
454           elsif !owner_class.respond_to? :find_by_uuid
455             raise ActiveRecord::RecordNotFound.new
456           else
457             x = owner_class.find_by_uuid(x).owner_uuid
458           end
459         rescue ActiveRecord::RecordNotFound => e
460           errors.add :owner_uuid, "is not owned by any user: #{e}"
461           throw(:abort)
462         end
463         if uuid_in_path[x]
464           if x == owner_uuid
465             errors.add :owner_uuid, "would create an ownership cycle"
466           else
467             errors.add :owner_uuid, "has an ownership cycle"
468           end
469           throw(:abort)
470         end
471         uuid_in_path[x] = true
472       end
473     end
474     true
475   end
476
477   def set_default_owner
478     if new_record? and current_user and respond_to? :owner_uuid=
479       self.owner_uuid ||= current_user.uuid
480     end
481   end
482
483   def ensure_owner_uuid_is_permitted
484     raise PermissionDeniedError if !current_user
485
486     if self.owner_uuid.nil?
487       errors.add :owner_uuid, "cannot be nil"
488       raise PermissionDeniedError
489     end
490
491     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
492     unless rsc_class == User or rsc_class == Group
493       errors.add :owner_uuid, "must be set to User or Group"
494       raise PermissionDeniedError
495     end
496
497     if new_record? || owner_uuid_changed?
498       # Permission on owner_uuid_was is needed to move an existing
499       # object away from its previous owner (which implies permission
500       # to modify this object itself, so we don't need to check that
501       # separately). Permission on the new owner_uuid is also needed.
502       [['old', owner_uuid_was],
503        ['new', owner_uuid]
504       ].each do |which, check_uuid|
505         if check_uuid.nil?
506           # old_owner_uuid is nil? New record, no need to check.
507         elsif !current_user.can?(write: check_uuid)
508           logger.warn "User #{current_user.uuid} tried to set ownership of #{self.class.to_s} #{self.uuid} but does not have permission to write #{which} owner_uuid #{check_uuid}"
509           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
510           raise PermissionDeniedError
511         end
512       end
513     else
514       # If the object already existed and we're not changing
515       # owner_uuid, we only need write permission on the object
516       # itself.
517       if !current_user.can?(write: self.uuid)
518         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
519         errors.add :uuid, "is not writable"
520         raise PermissionDeniedError
521       end
522     end
523
524     true
525   end
526
527   def ensure_permission_to_save
528     unless (new_record? ? permission_to_create : permission_to_update)
529       raise PermissionDeniedError
530     end
531   end
532
533   def permission_to_create
534     current_user.andand.is_active
535   end
536
537   def permission_to_update
538     if !current_user
539       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
540       return false
541     end
542     if !current_user.is_active
543       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
544       return false
545     end
546     return true if current_user.is_admin
547     if self.uuid_changed?
548       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
549       return false
550     end
551     return true
552   end
553
554   def ensure_permission_to_destroy
555     raise PermissionDeniedError unless permission_to_destroy
556   end
557
558   def permission_to_destroy
559     permission_to_update
560   end
561
562   def maybe_update_modified_by_fields
563     update_modified_by_fields if self.changed? or self.new_record?
564     true
565   end
566
567   def update_modified_by_fields
568     current_time = db_current_time
569     self.created_at ||= created_at_was || current_time
570     self.updated_at = current_time
571     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
572     if !anonymous_updater
573       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
574     end
575     if !timeless_updater
576       self.modified_at = current_time
577     end
578     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
579     true
580   end
581
582   def self.has_nonstring_keys? x
583     if x.is_a? Hash
584       x.each do |k,v|
585         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
586       end
587     elsif x.is_a? Array
588       x.each do |v|
589         return true if has_nonstring_keys?(v)
590       end
591     end
592     false
593   end
594
595   def self.has_symbols? x
596     if x.is_a? Hash
597       x.each do |k,v|
598         return true if has_symbols?(k) or has_symbols?(v)
599       end
600     elsif x.is_a? Array
601       x.each do |k|
602         return true if has_symbols?(k)
603       end
604     elsif x.is_a? Symbol
605       return true
606     elsif x.is_a? String
607       return true if x.start_with?(':') && !x.start_with?('::')
608     end
609     false
610   end
611
612   def self.recursive_stringify x
613     if x.is_a? Hash
614       Hash[x.collect do |k,v|
615              [recursive_stringify(k), recursive_stringify(v)]
616            end]
617     elsif x.is_a? Array
618       x.collect do |k|
619         recursive_stringify k
620       end
621     elsif x.is_a? Symbol
622       x.to_s
623     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
624       x[1..-1]
625     else
626       x
627     end
628   end
629
630   def self.where_serialized(colname, value, md5: false)
631     colsql = colname.to_s
632     if md5
633       colsql = "md5(#{colsql})"
634     end
635     if value.empty?
636       # rails4 stores as null, rails3 stored as serialized [] or {}
637       sql = "#{colsql} is null or #{colsql} IN (?)"
638       sorted = value
639     else
640       sql = "#{colsql} IN (?)"
641       sorted = deep_sort_hash(value)
642     end
643     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
644     if md5
645       params = params.map { |x| Digest::MD5.hexdigest(x) }
646     end
647     where(sql, params)
648   end
649
650   Serializer = {
651     Hash => HashSerializer,
652     Array => ArraySerializer,
653   }
654
655   def self.serialize(colname, type)
656     coder = Serializer[type]
657     @serialized_attributes ||= {}
658     @serialized_attributes[colname.to_s] = coder
659     super(colname, coder)
660   end
661
662   def self.serialized_attributes
663     @serialized_attributes ||= {}
664   end
665
666   def serialized_attributes
667     self.class.serialized_attributes
668   end
669
670   def convert_serialized_symbols_to_strings
671     # ensure_serialized_attribute_type should prevent symbols from
672     # getting into the database in the first place. If someone managed
673     # to get them into the database (perhaps using an older version)
674     # we'll convert symbols to strings when loading from the
675     # database. (Otherwise, loading and saving an object with existing
676     # symbols in a serialized field will crash.)
677     self.class.serialized_attributes.each do |colname, attr|
678       if self.class.has_symbols? attributes[colname]
679         attributes[colname] = self.class.recursive_stringify attributes[colname]
680         send(colname + '=',
681              self.class.recursive_stringify(attributes[colname]))
682       end
683     end
684   end
685
686   def foreign_key_attributes
687     attributes.keys.select { |a| a.match(/_uuid$/) }
688   end
689
690   def skip_uuid_read_permission_check
691     %w(modified_by_client_uuid)
692   end
693
694   def skip_uuid_existence_check
695     []
696   end
697
698   def normalize_collection_uuids
699     foreign_key_attributes.each do |attr|
700       attr_value = send attr
701       if attr_value.is_a? String and
702           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
703         begin
704           send "#{attr}=", Collection.normalize_uuid(attr_value)
705         rescue
706           # TODO: abort instead of silently accepting unnormalizable value?
707         end
708       end
709     end
710   end
711
712   @@prefixes_hash = nil
713   def self.uuid_prefixes
714     unless @@prefixes_hash
715       @@prefixes_hash = {}
716       Rails.application.eager_load!
717       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
718         if k.respond_to?(:uuid_prefix)
719           @@prefixes_hash[k.uuid_prefix] = k
720         end
721       end
722     end
723     @@prefixes_hash
724   end
725
726   def self.uuid_like_pattern
727     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
728   end
729
730   def self.uuid_regex
731     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
732   end
733
734   def ensure_valid_uuids
735     specials = [system_user_uuid]
736
737     foreign_key_attributes.each do |attr|
738       if new_record? or send (attr + "_changed?")
739         next if skip_uuid_existence_check.include? attr
740         attr_value = send attr
741         next if specials.include? attr_value
742         if attr_value
743           if (r = ArvadosModel::resource_class_for_uuid attr_value)
744             unless skip_uuid_read_permission_check.include? attr
745               r = r.readable_by(current_user)
746             end
747             if r.where(uuid: attr_value).count == 0
748               errors.add(attr, "'#{attr_value}' not found")
749             end
750           end
751         end
752       end
753     end
754   end
755
756   class Email
757     def self.kind
758       "email"
759     end
760
761     def kind
762       self.class.kind
763     end
764
765     def self.readable_by (*u)
766       self
767     end
768
769     def self.where (u)
770       [{:uuid => u[:uuid]}]
771     end
772   end
773
774   def self.resource_class_for_uuid(uuid)
775     if uuid.is_a? ArvadosModel
776       return uuid.class
777     end
778     unless uuid.is_a? String
779       return nil
780     end
781
782     uuid.match HasUuid::UUID_REGEX do |re|
783       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
784     end
785
786     if uuid.match(/.+@.+/)
787       return Email
788     end
789
790     nil
791   end
792
793   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
794   # an object in any class.
795   def self.find_by_uuid uuid
796     if self == ArvadosModel
797       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
798       # delegate to the appropriate subclass based on the given uuid.
799       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
800     else
801       super
802     end
803   end
804
805   def is_audit_logging_enabled?
806     return !(Rails.configuration.max_audit_log_age.to_i == 0 &&
807              Rails.configuration.max_audit_log_delete_batch.to_i > 0)
808   end
809
810   def log_start_state
811     if is_audit_logging_enabled?
812       @old_attributes = Marshal.load(Marshal.dump(attributes))
813       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
814     end
815   end
816
817   def log_change(event_type)
818     if is_audit_logging_enabled?
819       log = Log.new(event_type: event_type).fill_object(self)
820       yield log
821       log.save!
822       log_start_state
823     end
824   end
825
826   def log_create
827     if is_audit_logging_enabled?
828       log_change('create') do |log|
829         log.fill_properties('old', nil, nil)
830         log.update_to self
831       end
832     end
833   end
834
835   def log_update
836     if is_audit_logging_enabled?
837       log_change('update') do |log|
838         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
839         log.update_to self
840       end
841     end
842   end
843
844   def log_destroy
845     if is_audit_logging_enabled?
846       log_change('delete') do |log|
847         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
848         log.update_to nil
849       end
850     end
851   end
852 end