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