12032: Use permission_view in subquery to filter objects readable by user.
[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_trashed = kwargs.fetch(:include_trashed, 0)
258
259     sql_conds = []
260     user_uuids = users_list.map { |u| u.uuid }
261
262     User.install_view('permission')
263
264     # Check if any of the users are admin.
265     if users_list.select { |u| u.is_admin }.any?
266       # For admins, only filter on "trashed"
267       # sql_conds += ["#{sql_table}.uuid in (SELECT target_uuid
268       #             FROM permission_view
269       #             WHERE trashed in (:include_trashed)
270       #             GROUP BY user_uuid, target_uuid)"]
271
272       # if self.column_names.include? 'owner_uuid'
273       #   sql_conds[0] += "AND #{sql_table}.owner_uuid in (SELECT target_uuid
274       #             FROM permission_view
275       #             WHERE trashed in (:include_trashed)
276       #             GROUP BY user_uuid, target_uuid)"
277       # end
278       return where({})
279     else
280       # Match any object (evidently a group or user) whose UUID is
281       # listed explicitly in user_uuids.
282       sql_conds += ["#{sql_table}.uuid in (:user_uuids)"]
283
284       # Match any object whose owner is listed explicitly in
285       # user_uuids.
286       sql_conds += ["#{sql_table}.owner_uuid IN (:user_uuids)"]
287
288       # At least read permission from user_uuid to target_uuid of object
289       sql_conds += ["#{sql_table}.uuid in (SELECT target_uuid
290                   FROM permission_view
291                   WHERE user_uuid in (:user_uuids) and perm_level >= 1 and trashed = (:include_trashed)
292                   GROUP BY user_uuid, target_uuid)"]
293
294       if self.column_names.include? 'owner_uuid'
295         # At least read permission from user_uuid to target_uuid that owns object
296         sql_conds += ["#{sql_table}.owner_uuid in (SELECT target_uuid
297                   FROM permission_view
298                   WHERE user_uuid in (:user_uuids) and
299                     target_owner_uuid IS NOT NULL and
300                     perm_level >= 1 and trashed = (:include_trashed)
301                   GROUP BY user_uuid, target_uuid)"]
302       end
303
304       if sql_table == "links"
305         # Match any permission link that gives one of the authorized
306         # users some permission _or_ gives anyone else permission to
307         # view one of the authorized users.
308         sql_conds += ["(#{sql_table}.link_class in (:permission_link_classes) AND "+
309                       "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"]
310       end
311     end
312
313     where(sql_conds.join(' OR '),
314           user_uuids: user_uuids,
315           permission_link_classes: ['permission', 'resources'],
316           include_trashed: include_trashed)
317   end
318
319   def save_with_unique_name!
320     uuid_was = uuid
321     name_was = name
322     max_retries = 2
323     transaction do
324       conn = ActiveRecord::Base.connection
325       conn.exec_query 'SAVEPOINT save_with_unique_name'
326       begin
327         save!
328       rescue ActiveRecord::RecordNotUnique => rn
329         raise if max_retries == 0
330         max_retries -= 1
331
332         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
333
334         # Dig into the error to determine if it is specifically calling out a
335         # (owner_uuid, name) uniqueness violation.  In this specific case, and
336         # the client requested a unique name with ensure_unique_name==true,
337         # update the name field and try to save again.  Loop as necessary to
338         # discover a unique name.  It is necessary to handle name choosing at
339         # this level (as opposed to the client) to ensure that record creation
340         # never fails due to a race condition.
341         err = rn.original_exception
342         raise unless err.is_a?(PG::UniqueViolation)
343
344         # Unfortunately ActiveRecord doesn't abstract out any of the
345         # necessary information to figure out if this the error is actually
346         # the specific case where we want to apply the ensure_unique_name
347         # behavior, so the following code is specialized to Postgres.
348         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
349         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
350
351         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
352         if new_name == name
353           # If the database is fast enough to do two attempts in the
354           # same millisecond, we need to wait to ensure we try a
355           # different timestamp on each attempt.
356           sleep 0.002
357           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
358         end
359
360         self[:name] = new_name
361         self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
362         conn.exec_query 'SAVEPOINT save_with_unique_name'
363         retry
364       ensure
365         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
366       end
367     end
368   end
369
370   def logged_attributes
371     attributes.except(*Rails.configuration.unlogged_attributes)
372   end
373
374   def self.full_text_searchable_columns
375     self.columns.select do |col|
376       col.type == :string or col.type == :text
377     end.map(&:name)
378   end
379
380   def self.full_text_tsvector
381     parts = full_text_searchable_columns.collect do |column|
382       "coalesce(#{column},'')"
383     end
384     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
385   end
386
387   def self.apply_filters query, filters
388     ft = record_filters filters, self
389     if not ft[:cond_out].any?
390       return query
391     end
392     query.where('(' + ft[:cond_out].join(') AND (') + ')',
393                           *ft[:param_out])
394   end
395
396   protected
397
398   def self.deep_sort_hash(x)
399     if x.is_a? Hash
400       x.sort.collect do |k, v|
401         [k, deep_sort_hash(v)]
402       end.to_h
403     elsif x.is_a? Array
404       x.collect { |v| deep_sort_hash(v) }
405     else
406       x
407     end
408   end
409
410   def ensure_ownership_path_leads_to_user
411     if new_record? or owner_uuid_changed?
412       uuid_in_path = {owner_uuid => true, uuid => true}
413       x = owner_uuid
414       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
415         begin
416           if x == uuid
417             # Test for cycles with the new version, not the DB contents
418             x = owner_uuid
419           elsif !owner_class.respond_to? :find_by_uuid
420             raise ActiveRecord::RecordNotFound.new
421           else
422             x = owner_class.find_by_uuid(x).owner_uuid
423           end
424         rescue ActiveRecord::RecordNotFound => e
425           errors.add :owner_uuid, "is not owned by any user: #{e}"
426           return false
427         end
428         if uuid_in_path[x]
429           if x == owner_uuid
430             errors.add :owner_uuid, "would create an ownership cycle"
431           else
432             errors.add :owner_uuid, "has an ownership cycle"
433           end
434           return false
435         end
436         uuid_in_path[x] = true
437       end
438     end
439     true
440   end
441
442   def set_default_owner
443     if new_record? and current_user and respond_to? :owner_uuid=
444       self.owner_uuid ||= current_user.uuid
445     end
446   end
447
448   def ensure_owner_uuid_is_permitted
449     raise PermissionDeniedError if !current_user
450
451     if self.owner_uuid.nil?
452       errors.add :owner_uuid, "cannot be nil"
453       raise PermissionDeniedError
454     end
455
456     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
457     unless rsc_class == User or rsc_class == Group
458       errors.add :owner_uuid, "must be set to User or Group"
459       raise PermissionDeniedError
460     end
461
462     if new_record? || owner_uuid_changed?
463       # Permission on owner_uuid_was is needed to move an existing
464       # object away from its previous owner (which implies permission
465       # to modify this object itself, so we don't need to check that
466       # separately). Permission on the new owner_uuid is also needed.
467       [['old', owner_uuid_was],
468        ['new', owner_uuid]
469       ].each do |which, check_uuid|
470         if check_uuid.nil?
471           # old_owner_uuid is nil? New record, no need to check.
472         elsif !current_user.can?(write: check_uuid)
473           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}"
474           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
475           raise PermissionDeniedError
476         end
477       end
478     else
479       # If the object already existed and we're not changing
480       # owner_uuid, we only need write permission on the object
481       # itself.
482       if !current_user.can?(write: self.uuid)
483         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
484         errors.add :uuid, "is not writable"
485         raise PermissionDeniedError
486       end
487     end
488
489     true
490   end
491
492   def ensure_permission_to_save
493     unless (new_record? ? permission_to_create : permission_to_update)
494       raise PermissionDeniedError
495     end
496   end
497
498   def permission_to_create
499     current_user.andand.is_active
500   end
501
502   def permission_to_update
503     if !current_user
504       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
505       return false
506     end
507     if !current_user.is_active
508       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
509       return false
510     end
511     return true if current_user.is_admin
512     if self.uuid_changed?
513       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
514       return false
515     end
516     return true
517   end
518
519   def ensure_permission_to_destroy
520     raise PermissionDeniedError unless permission_to_destroy
521   end
522
523   def permission_to_destroy
524     permission_to_update
525   end
526
527   def maybe_update_modified_by_fields
528     update_modified_by_fields if self.changed? or self.new_record?
529     true
530   end
531
532   def update_modified_by_fields
533     current_time = db_current_time
534     self.created_at = created_at_was || current_time
535     self.updated_at = current_time
536     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
537     self.modified_at = current_time
538     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
539     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
540     true
541   end
542
543   def self.has_nonstring_keys? x
544     if x.is_a? Hash
545       x.each do |k,v|
546         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
547       end
548     elsif x.is_a? Array
549       x.each do |v|
550         return true if has_nonstring_keys?(v)
551       end
552     end
553     false
554   end
555
556   def self.has_symbols? x
557     if x.is_a? Hash
558       x.each do |k,v|
559         return true if has_symbols?(k) or has_symbols?(v)
560       end
561     elsif x.is_a? Array
562       x.each do |k|
563         return true if has_symbols?(k)
564       end
565     elsif x.is_a? Symbol
566       return true
567     elsif x.is_a? String
568       return true if x.start_with?(':') && !x.start_with?('::')
569     end
570     false
571   end
572
573   def self.recursive_stringify x
574     if x.is_a? Hash
575       Hash[x.collect do |k,v|
576              [recursive_stringify(k), recursive_stringify(v)]
577            end]
578     elsif x.is_a? Array
579       x.collect do |k|
580         recursive_stringify k
581       end
582     elsif x.is_a? Symbol
583       x.to_s
584     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
585       x[1..-1]
586     else
587       x
588     end
589   end
590
591   def self.where_serialized(colname, value)
592     if value.empty?
593       # rails4 stores as null, rails3 stored as serialized [] or {}
594       sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)"
595       sorted = value
596     else
597       sql = "#{colname.to_s} IN (?)"
598       sorted = deep_sort_hash(value)
599     end
600     where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)])
601   end
602
603   Serializer = {
604     Hash => HashSerializer,
605     Array => ArraySerializer,
606   }
607
608   def self.serialize(colname, type)
609     coder = Serializer[type]
610     @serialized_attributes ||= {}
611     @serialized_attributes[colname.to_s] = coder
612     super(colname, coder)
613   end
614
615   def self.serialized_attributes
616     @serialized_attributes ||= {}
617   end
618
619   def serialized_attributes
620     self.class.serialized_attributes
621   end
622
623   def convert_serialized_symbols_to_strings
624     # ensure_serialized_attribute_type should prevent symbols from
625     # getting into the database in the first place. If someone managed
626     # to get them into the database (perhaps using an older version)
627     # we'll convert symbols to strings when loading from the
628     # database. (Otherwise, loading and saving an object with existing
629     # symbols in a serialized field will crash.)
630     self.class.serialized_attributes.each do |colname, attr|
631       if self.class.has_symbols? attributes[colname]
632         attributes[colname] = self.class.recursive_stringify attributes[colname]
633         send(colname + '=',
634              self.class.recursive_stringify(attributes[colname]))
635       end
636     end
637   end
638
639   def foreign_key_attributes
640     attributes.keys.select { |a| a.match(/_uuid$/) }
641   end
642
643   def skip_uuid_read_permission_check
644     %w(modified_by_client_uuid)
645   end
646
647   def skip_uuid_existence_check
648     []
649   end
650
651   def normalize_collection_uuids
652     foreign_key_attributes.each do |attr|
653       attr_value = send attr
654       if attr_value.is_a? String and
655           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
656         begin
657           send "#{attr}=", Collection.normalize_uuid(attr_value)
658         rescue
659           # TODO: abort instead of silently accepting unnormalizable value?
660         end
661       end
662     end
663   end
664
665   @@prefixes_hash = nil
666   def self.uuid_prefixes
667     unless @@prefixes_hash
668       @@prefixes_hash = {}
669       Rails.application.eager_load!
670       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
671         if k.respond_to?(:uuid_prefix)
672           @@prefixes_hash[k.uuid_prefix] = k
673         end
674       end
675     end
676     @@prefixes_hash
677   end
678
679   def self.uuid_like_pattern
680     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
681   end
682
683   def self.uuid_regex
684     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
685   end
686
687   def ensure_valid_uuids
688     specials = [system_user_uuid]
689
690     foreign_key_attributes.each do |attr|
691       if new_record? or send (attr + "_changed?")
692         next if skip_uuid_existence_check.include? attr
693         attr_value = send attr
694         next if specials.include? attr_value
695         if attr_value
696           if (r = ArvadosModel::resource_class_for_uuid attr_value)
697             unless skip_uuid_read_permission_check.include? attr
698               r = r.readable_by(current_user)
699             end
700             if r.where(uuid: attr_value).count == 0
701               errors.add(attr, "'#{attr_value}' not found")
702             end
703           end
704         end
705       end
706     end
707   end
708
709   class Email
710     def self.kind
711       "email"
712     end
713
714     def kind
715       self.class.kind
716     end
717
718     def self.readable_by (*u)
719       self
720     end
721
722     def self.where (u)
723       [{:uuid => u[:uuid]}]
724     end
725   end
726
727   def self.resource_class_for_uuid(uuid)
728     if uuid.is_a? ArvadosModel
729       return uuid.class
730     end
731     unless uuid.is_a? String
732       return nil
733     end
734
735     uuid.match HasUuid::UUID_REGEX do |re|
736       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
737     end
738
739     if uuid.match(/.+@.+/)
740       return Email
741     end
742
743     nil
744   end
745
746   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
747   # an object in any class.
748   def self.find_by_uuid uuid
749     if self == ArvadosModel
750       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
751       # delegate to the appropriate subclass based on the given uuid.
752       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
753     else
754       super
755     end
756   end
757
758   def log_start_state
759     @old_attributes = Marshal.load(Marshal.dump(attributes))
760     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
761   end
762
763   def log_change(event_type)
764     log = Log.new(event_type: event_type).fill_object(self)
765     yield log
766     log.save!
767     log_start_state
768   end
769
770   def log_create
771     log_change('create') do |log|
772       log.fill_properties('old', nil, nil)
773       log.update_to self
774     end
775   end
776
777   def log_update
778     log_change('update') do |log|
779       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
780       log.update_to self
781     end
782   end
783
784   def log_destroy
785     log_change('delete') do |log|
786       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
787       log.update_to nil
788     end
789   end
790 end