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