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