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